Add code of django-restframwork to taiga
parent
d3fade9565
commit
910d71eefc
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-04-06 17:04+0200\n"
|
"POT-Creation-Date: 2015-04-09 20:07+0200\n"
|
||||||
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
|
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||||
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
djangorestframework==2.3.13
|
|
||||||
Django==1.7.6
|
Django==1.7.6
|
||||||
|
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
|
||||||
django-picklefield==0.3.1
|
django-picklefield==0.3.1
|
||||||
django-sampledatahelper==0.2.2
|
django-sampledatahelper==0.2.2
|
||||||
gunicorn==19.1.1
|
gunicorn==19.1.1
|
||||||
|
|
|
@ -269,6 +269,7 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
|
|
||||||
"taiga.base",
|
"taiga.base",
|
||||||
|
"taiga.base.api",
|
||||||
"taiga.events",
|
"taiga.events",
|
||||||
"taiga.front",
|
"taiga.front",
|
||||||
"taiga.users",
|
"taiga.users",
|
||||||
|
@ -295,7 +296,6 @@ INSTALLED_APPS = [
|
||||||
"taiga.hooks.bitbucket",
|
"taiga.hooks.bitbucket",
|
||||||
"taiga.webhooks",
|
"taiga.webhooks",
|
||||||
|
|
||||||
"rest_framework",
|
|
||||||
"djmail",
|
"djmail",
|
||||||
"django_jinja",
|
"django_jinja",
|
||||||
"django_jinja.contrib._humanize",
|
"django_jinja.contrib._humanize",
|
||||||
|
|
|
@ -20,8 +20,7 @@ from enum import Enum
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.api import viewsets
|
from taiga.base.api import viewsets
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
|
|
|
@ -35,7 +35,7 @@ fraudulent modifications.
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework.authentication import BaseAuthentication
|
from taiga.base.api.authentication import BaseAuthentication
|
||||||
|
|
||||||
from .tokens import get_user_for_token
|
from .tokens import get_user_for_token
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ from .tokens import get_user_for_token
|
||||||
class Session(BaseAuthentication):
|
class Session(BaseAuthentication):
|
||||||
"""
|
"""
|
||||||
Session based authentication like the standard
|
Session based authentication like the standard
|
||||||
`rest_framework.authentication.SessionAuthentication`
|
`taiga.base.api.authentication.SessionAuthentication`
|
||||||
but with csrf disabled (for obvious reasons because
|
but with csrf disabled (for obvious reasons because
|
||||||
it is for api.
|
it is for api.
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,15 @@
|
||||||
# This code is partially taken from django-rest-framework:
|
# This code is partially taken from django-rest-framework:
|
||||||
# Copyright (c) 2011-2014, Tom Christie
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
VERSION = "2.3.13-taiga" # Based on django-resframework 2.3.13
|
||||||
|
|
||||||
|
# Header encoding (see RFC5987)
|
||||||
|
HTTP_HEADER_ENCODING = 'iso-8859-1'
|
||||||
|
|
||||||
|
# Default datetime input and output formats
|
||||||
|
ISO_8601 = 'iso-8601'
|
||||||
|
|
||||||
|
|
||||||
from .viewsets import ModelListViewSet
|
from .viewsets import ModelListViewSet
|
||||||
from .viewsets import ModelCrudViewSet
|
from .viewsets import ModelCrudViewSet
|
||||||
from .viewsets import ModelUpdateRetrieveViewSet
|
from .viewsets import ModelUpdateRetrieveViewSet
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Provides various authentication policies.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from django.contrib.auth import authenticate
|
||||||
|
from django.middleware.csrf import CsrfViewMiddleware
|
||||||
|
|
||||||
|
from taiga.base import exceptions
|
||||||
|
|
||||||
|
from . import HTTP_HEADER_ENCODING
|
||||||
|
|
||||||
|
|
||||||
|
def get_authorization_header(request):
|
||||||
|
"""
|
||||||
|
Return request's 'Authorization:' header, as a bytestring.
|
||||||
|
|
||||||
|
Hide some test client ickyness where the header can be unicode.
|
||||||
|
"""
|
||||||
|
auth = request.META.get('HTTP_AUTHORIZATION', b'')
|
||||||
|
if type(auth) == type(''):
|
||||||
|
# Work around django test client oddness
|
||||||
|
auth = auth.encode(HTTP_HEADER_ENCODING)
|
||||||
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
class CSRFCheck(CsrfViewMiddleware):
|
||||||
|
def _reject(self, request, reason):
|
||||||
|
# Return the failure reason instead of an HttpResponse
|
||||||
|
return reason
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAuthentication(object):
|
||||||
|
"""
|
||||||
|
All authentication classes should extend BaseAuthentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
"""
|
||||||
|
Authenticate the request and return a two-tuple of (user, token).
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(".authenticate() must be overridden.")
|
||||||
|
|
||||||
|
def authenticate_header(self, request):
|
||||||
|
"""
|
||||||
|
Return a string to be used as the value of the `WWW-Authenticate`
|
||||||
|
header in a `401 Unauthenticated` response, or `None` if the
|
||||||
|
authentication scheme should return `403 Permission Denied` responses.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BasicAuthentication(BaseAuthentication):
|
||||||
|
"""
|
||||||
|
HTTP Basic authentication against username/password.
|
||||||
|
"""
|
||||||
|
www_authenticate_realm = 'api'
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
"""
|
||||||
|
Returns a `User` if a correct username and password have been supplied
|
||||||
|
using HTTP Basic authentication. Otherwise returns `None`.
|
||||||
|
"""
|
||||||
|
auth = get_authorization_header(request).split()
|
||||||
|
|
||||||
|
if not auth or auth[0].lower() != b'basic':
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(auth) == 1:
|
||||||
|
msg = 'Invalid basic header. No credentials provided.'
|
||||||
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
elif len(auth) > 2:
|
||||||
|
msg = 'Invalid basic header. Credentials string should not contain spaces.'
|
||||||
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
|
||||||
|
except (TypeError, UnicodeDecodeError):
|
||||||
|
msg = 'Invalid basic header. Credentials not correctly base64 encoded'
|
||||||
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
|
userid, password = auth_parts[0], auth_parts[2]
|
||||||
|
return self.authenticate_credentials(userid, password)
|
||||||
|
|
||||||
|
def authenticate_credentials(self, userid, password):
|
||||||
|
"""
|
||||||
|
Authenticate the userid and password against username and password.
|
||||||
|
"""
|
||||||
|
user = authenticate(username=userid, password=password)
|
||||||
|
if user is None or not user.is_active:
|
||||||
|
raise exceptions.AuthenticationFailed('Invalid username/password')
|
||||||
|
return (user, None)
|
||||||
|
|
||||||
|
def authenticate_header(self, request):
|
||||||
|
return 'Basic realm="%s"' % self.www_authenticate_realm
|
||||||
|
|
||||||
|
|
||||||
|
class SessionAuthentication(BaseAuthentication):
|
||||||
|
"""
|
||||||
|
Use Django's session framework for authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
"""
|
||||||
|
Returns a `User` if the request session currently has a logged in user.
|
||||||
|
Otherwise returns `None`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the underlying HttpRequest object
|
||||||
|
request = request._request
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
|
||||||
|
# Unauthenticated, CSRF validation not required
|
||||||
|
if not user or not user.is_active:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.enforce_csrf(request)
|
||||||
|
|
||||||
|
# CSRF passed with authenticated user
|
||||||
|
return (user, None)
|
||||||
|
|
||||||
|
def enforce_csrf(self, request):
|
||||||
|
"""
|
||||||
|
Enforce CSRF validation for session based authentication.
|
||||||
|
"""
|
||||||
|
reason = CSRFCheck().process_view(request, None, (), {})
|
||||||
|
if reason:
|
||||||
|
# CSRF failed, bail with explicit error message
|
||||||
|
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
|
File diff suppressed because it is too large
Load Diff
|
@ -17,33 +17,18 @@
|
||||||
# This code is partially taken from django-rest-framework:
|
# This code is partially taken from django-rest-framework:
|
||||||
# Copyright (c) 2011-2014, Tom Christie
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.paginator import Paginator, InvalidPage
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
|
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
from . import mixins
|
from . import mixins
|
||||||
|
from . import pagination
|
||||||
|
from .settings import api_settings
|
||||||
from .utils import get_object_or_404
|
from .utils import get_object_or_404
|
||||||
|
|
||||||
|
|
||||||
def strict_positive_int(integer_string, cutoff=None):
|
class GenericAPIView(pagination.PaginationMixin,
|
||||||
"""
|
views.APIView):
|
||||||
Cast a string to a strictly positive integer.
|
|
||||||
"""
|
|
||||||
ret = int(integer_string)
|
|
||||||
if ret <= 0:
|
|
||||||
raise ValueError()
|
|
||||||
if cutoff:
|
|
||||||
ret = min(ret, cutoff)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class GenericAPIView(views.APIView):
|
|
||||||
"""
|
"""
|
||||||
Base class for all other generic views.
|
Base class for all other generic views.
|
||||||
"""
|
"""
|
||||||
|
@ -63,20 +48,12 @@ class GenericAPIView(views.APIView):
|
||||||
lookup_field = 'pk'
|
lookup_field = 'pk'
|
||||||
lookup_url_kwarg = None
|
lookup_url_kwarg = None
|
||||||
|
|
||||||
# Pagination settings
|
|
||||||
paginate_by = api_settings.PAGINATE_BY
|
|
||||||
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
|
||||||
max_paginate_by = api_settings.MAX_PAGINATE_BY
|
|
||||||
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
|
|
||||||
page_kwarg = 'page'
|
|
||||||
|
|
||||||
# The filter backend classes to use for queryset filtering
|
# The filter backend classes to use for queryset filtering
|
||||||
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
|
||||||
|
|
||||||
# The following attributes may be subject to change,
|
# The following attributes may be subject to change,
|
||||||
# and should be considered private API.
|
# and should be considered private API.
|
||||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||||
paginator_class = Paginator
|
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
# These are pending deprecation...
|
# These are pending deprecation...
|
||||||
|
@ -107,68 +84,6 @@ class GenericAPIView(views.APIView):
|
||||||
return serializer_class(instance, data=data, files=files,
|
return serializer_class(instance, data=data, files=files,
|
||||||
many=many, partial=partial, context=context)
|
many=many, partial=partial, context=context)
|
||||||
|
|
||||||
def get_pagination_serializer(self, page):
|
|
||||||
"""
|
|
||||||
Return a serializer instance to use with paginated data.
|
|
||||||
"""
|
|
||||||
class SerializerClass(self.pagination_serializer_class):
|
|
||||||
class Meta:
|
|
||||||
object_serializer_class = self.get_serializer_class()
|
|
||||||
|
|
||||||
pagination_serializer_class = SerializerClass
|
|
||||||
context = self.get_serializer_context()
|
|
||||||
return pagination_serializer_class(instance=page, context=context)
|
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, page_size=None):
|
|
||||||
"""
|
|
||||||
Paginate a queryset if required, either returning a page object,
|
|
||||||
or `None` if pagination is not configured for this view.
|
|
||||||
"""
|
|
||||||
deprecated_style = False
|
|
||||||
if page_size is not None:
|
|
||||||
warnings.warn(_('The `page_size` parameter to `paginate_queryset()` '
|
|
||||||
'is due to be deprecated. '
|
|
||||||
'Note that the return style of this method is also '
|
|
||||||
'changed, and will simply return a page object '
|
|
||||||
'when called without a `page_size` argument.'),
|
|
||||||
PendingDeprecationWarning, stacklevel=2)
|
|
||||||
deprecated_style = True
|
|
||||||
else:
|
|
||||||
# Determine the required page size.
|
|
||||||
# If pagination is not configured, simply return None.
|
|
||||||
page_size = self.get_paginate_by()
|
|
||||||
if not page_size:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not self.allow_empty:
|
|
||||||
warnings.warn(_('The `allow_empty` parameter is due to be deprecated. '
|
|
||||||
'To use `allow_empty=False` style behavior, You should override '
|
|
||||||
'`get_queryset()` and explicitly raise a 404 on empty querysets.'),
|
|
||||||
PendingDeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
paginator = self.paginator_class(queryset, page_size,
|
|
||||||
allow_empty_first_page=self.allow_empty)
|
|
||||||
page_kwarg = self.kwargs.get(self.page_kwarg)
|
|
||||||
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
|
|
||||||
page = page_kwarg or page_query_param or 1
|
|
||||||
try:
|
|
||||||
page_number = paginator.validate_number(page)
|
|
||||||
except InvalidPage:
|
|
||||||
if page == 'last':
|
|
||||||
page_number = paginator.num_pages
|
|
||||||
else:
|
|
||||||
raise Http404(_("Page is not 'last', nor can it be converted to an int."))
|
|
||||||
try:
|
|
||||||
page = paginator.page(page_number)
|
|
||||||
except InvalidPage as e:
|
|
||||||
raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
|
|
||||||
'page_number': page_number,
|
|
||||||
'message': str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
if deprecated_style:
|
|
||||||
return (paginator, page, page.object_list, page.has_other_pages())
|
|
||||||
return page
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
"""
|
"""
|
||||||
|
@ -189,10 +104,10 @@ class GenericAPIView(views.APIView):
|
||||||
"""
|
"""
|
||||||
filter_backends = self.filter_backends or []
|
filter_backends = self.filter_backends or []
|
||||||
if not filter_backends and hasattr(self, 'filter_backend'):
|
if not filter_backends and hasattr(self, 'filter_backend'):
|
||||||
raise RuntimeError(_('The `filter_backend` attribute and `FILTER_BACKEND` setting '
|
raise RuntimeError('The `filter_backend` attribute and `FILTER_BACKEND` setting '
|
||||||
'are due to be deprecated in favor of a `filter_backends` '
|
'are due to be deprecated in favor of a `filter_backends` '
|
||||||
'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
|
'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
|
||||||
'a *list* of filter backend classes.'))
|
'a *list* of filter backend classes.')
|
||||||
return filter_backends
|
return filter_backends
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
|
@ -200,29 +115,6 @@ class GenericAPIView(views.APIView):
|
||||||
# that you may want to override for more complex cases. #
|
# that you may want to override for more complex cases. #
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
def get_paginate_by(self, queryset=None):
|
|
||||||
"""
|
|
||||||
Return the size of pages to use with pagination.
|
|
||||||
|
|
||||||
If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
|
|
||||||
from a named query parameter in the url, eg. ?page_size=100
|
|
||||||
|
|
||||||
Otherwise defaults to using `self.paginate_by`.
|
|
||||||
"""
|
|
||||||
if queryset is not None:
|
|
||||||
raise RuntimeError(_('The `queryset` parameter to `get_paginate_by()` '
|
|
||||||
'is due to be deprecated.'))
|
|
||||||
if self.paginate_by_param:
|
|
||||||
try:
|
|
||||||
return strict_positive_int(
|
|
||||||
self.request.QUERY_PARAMS[self.paginate_by_param],
|
|
||||||
cutoff=self.max_paginate_by
|
|
||||||
)
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self.paginate_by
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
if self.action == "list" and hasattr(self, "list_serializer_class"):
|
if self.action == "list" and hasattr(self, "list_serializer_class"):
|
||||||
return self.list_serializer_class
|
return self.list_serializer_class
|
||||||
|
@ -231,7 +123,7 @@ class GenericAPIView(views.APIView):
|
||||||
if serializer_class is not None:
|
if serializer_class is not None:
|
||||||
return serializer_class
|
return serializer_class
|
||||||
|
|
||||||
assert self.model is not None, _("'%s' should either include a 'serializer_class' attribute, "
|
assert self.model is not None, ("'%s' should either include a 'serializer_class' attribute, "
|
||||||
"or use the 'model' attribute as a shortcut for "
|
"or use the 'model' attribute as a shortcut for "
|
||||||
"automatically generating a serializer class." % self.__class__.__name__)
|
"automatically generating a serializer class." % self.__class__.__name__)
|
||||||
|
|
||||||
|
@ -257,7 +149,7 @@ class GenericAPIView(views.APIView):
|
||||||
if self.model is not None:
|
if self.model is not None:
|
||||||
return self.model._default_manager.all()
|
return self.model._default_manager.all()
|
||||||
|
|
||||||
raise ImproperlyConfigured(_("'%s' must define 'queryset' or 'model'" % self.__class__.__name__))
|
raise ImproperlyConfigured(("'%s' must define 'queryset' or 'model'" % self.__class__.__name__))
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
"""
|
"""
|
||||||
|
@ -285,13 +177,13 @@ class GenericAPIView(views.APIView):
|
||||||
if lookup is not None:
|
if lookup is not None:
|
||||||
filter_kwargs = {self.lookup_field: lookup}
|
filter_kwargs = {self.lookup_field: lookup}
|
||||||
elif pk is not None and self.lookup_field == 'pk':
|
elif pk is not None and self.lookup_field == 'pk':
|
||||||
raise RuntimeError(_('The `pk_url_kwarg` attribute is due to be deprecated. '
|
raise RuntimeError(('The `pk_url_kwarg` attribute is due to be deprecated. '
|
||||||
'Use the `lookup_field` attribute instead'))
|
'Use the `lookup_field` attribute instead'))
|
||||||
elif slug is not None and self.lookup_field == 'pk':
|
elif slug is not None and self.lookup_field == 'pk':
|
||||||
raise RuntimeError(_('The `slug_url_kwarg` attribute is due to be deprecated. '
|
raise RuntimeError(('The `slug_url_kwarg` attribute is due to be deprecated. '
|
||||||
'Use the `lookup_field` attribute instead'))
|
'Use the `lookup_field` attribute instead'))
|
||||||
else:
|
else:
|
||||||
raise ImproperlyConfigured(_('Expected view %s to be called with a URL keyword argument '
|
raise ImproperlyConfigured(('Expected view %s to be called with a URL keyword argument '
|
||||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||||
'attribute on the view correctly.' %
|
'attribute on the view correctly.' %
|
||||||
(self.__class__.__name__, self.lookup_field)))
|
(self.__class__.__name__, self.lookup_field)))
|
||||||
|
|
|
@ -25,8 +25,8 @@ from django.db import transaction as tx
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
|
|
||||||
|
from .settings import api_settings
|
||||||
from .utils import get_object_or_404
|
from .utils import get_object_or_404
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Content negotiation deals with selecting an appropriate renderer given the
|
||||||
|
incoming request. Typically this will be based on the request's Accept header.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
|
from taiga.base import exceptions
|
||||||
|
from .settings import api_settings
|
||||||
|
|
||||||
|
from .utils.mediatypes import order_by_precedence
|
||||||
|
from .utils.mediatypes import media_type_matches
|
||||||
|
from .utils.mediatypes import _MediaType
|
||||||
|
|
||||||
|
|
||||||
|
class BaseContentNegotiation(object):
|
||||||
|
def select_parser(self, request, parsers):
|
||||||
|
raise NotImplementedError(".select_parser() must be implemented")
|
||||||
|
|
||||||
|
def select_renderer(self, request, renderers, format_suffix=None):
|
||||||
|
raise NotImplementedError(".select_renderer() must be implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultContentNegotiation(BaseContentNegotiation):
|
||||||
|
settings = api_settings
|
||||||
|
|
||||||
|
def select_parser(self, request, parsers):
|
||||||
|
"""
|
||||||
|
Given a list of parsers and a media type, return the appropriate
|
||||||
|
parser to handle the incoming request.
|
||||||
|
"""
|
||||||
|
for parser in parsers:
|
||||||
|
if media_type_matches(parser.media_type, request.content_type):
|
||||||
|
return parser
|
||||||
|
return None
|
||||||
|
|
||||||
|
def select_renderer(self, request, renderers, format_suffix=None):
|
||||||
|
"""
|
||||||
|
Given a request and a list of renderers, return a two-tuple of:
|
||||||
|
(renderer, media type).
|
||||||
|
"""
|
||||||
|
# Allow URL style format override. eg. "?format=json
|
||||||
|
format_query_param = self.settings.URL_FORMAT_OVERRIDE
|
||||||
|
format = format_suffix or request.QUERY_PARAMS.get(format_query_param)
|
||||||
|
|
||||||
|
if format:
|
||||||
|
renderers = self.filter_renderers(renderers, format)
|
||||||
|
|
||||||
|
accepts = self.get_accept_list(request)
|
||||||
|
|
||||||
|
# Check the acceptable media types against each renderer,
|
||||||
|
# attempting more specific media types first
|
||||||
|
# NB. The inner loop here isni't as bad as it first looks :)
|
||||||
|
# Worst case is we"re looping over len(accept_list) * len(self.renderers)
|
||||||
|
for media_type_set in order_by_precedence(accepts):
|
||||||
|
for renderer in renderers:
|
||||||
|
for media_type in media_type_set:
|
||||||
|
if media_type_matches(renderer.media_type, media_type):
|
||||||
|
# Return the most specific media type as accepted.
|
||||||
|
if (_MediaType(renderer.media_type).precedence >
|
||||||
|
_MediaType(media_type).precedence):
|
||||||
|
# Eg client requests "*/*"
|
||||||
|
# Accepted media type is "application/json"
|
||||||
|
return renderer, renderer.media_type
|
||||||
|
else:
|
||||||
|
# Eg client requests "application/json; indent=8"
|
||||||
|
# Accepted media type is "application/json; indent=8"
|
||||||
|
return renderer, media_type
|
||||||
|
|
||||||
|
raise exceptions.NotAcceptable(available_renderers=renderers)
|
||||||
|
|
||||||
|
def filter_renderers(self, renderers, format):
|
||||||
|
"""
|
||||||
|
If there is a ".json" style format suffix, filter the renderers
|
||||||
|
so that we only negotiation against those that accept that format.
|
||||||
|
"""
|
||||||
|
renderers = [renderer for renderer in renderers
|
||||||
|
if renderer.format == format]
|
||||||
|
if not renderers:
|
||||||
|
raise Http404
|
||||||
|
return renderers
|
||||||
|
|
||||||
|
def get_accept_list(self, request):
|
||||||
|
"""
|
||||||
|
Given the incoming request, return a tokenised list of media
|
||||||
|
type strings.
|
||||||
|
|
||||||
|
Allows URL style accept override. eg. "?accept=application/json"
|
||||||
|
"""
|
||||||
|
header = request.META.get("HTTP_ACCEPT", "*/*")
|
||||||
|
header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header)
|
||||||
|
return [token.strip() for token in header.split(",")]
|
|
@ -14,19 +14,112 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework.templatetags.rest_framework import replace_query_param
|
from django.core.paginator import Paginator, InvalidPage
|
||||||
|
from django.http import Http404
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from .settings import api_settings
|
||||||
|
from .templatetags.api import replace_query_param
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class ConditionalPaginationMixin(object):
|
def strict_positive_int(integer_string, cutoff=None):
|
||||||
def get_paginate_by(self, *args, **kwargs):
|
"""
|
||||||
|
Cast a string to a strictly positive integer.
|
||||||
|
"""
|
||||||
|
ret = int(integer_string)
|
||||||
|
if ret <= 0:
|
||||||
|
raise ValueError()
|
||||||
|
if cutoff:
|
||||||
|
ret = min(ret, cutoff)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class PaginationMixin(object):
|
||||||
|
# Pagination settings
|
||||||
|
paginate_by = api_settings.PAGINATE_BY
|
||||||
|
paginate_by_param = api_settings.PAGINATE_BY_PARAM
|
||||||
|
max_paginate_by = api_settings.MAX_PAGINATE_BY
|
||||||
|
page_kwarg = 'page'
|
||||||
|
paginator_class = Paginator
|
||||||
|
|
||||||
|
def get_paginate_by(self, queryset=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Return the size of pages to use with pagination.
|
||||||
|
|
||||||
|
If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
|
||||||
|
from a named query parameter in the url, eg. ?page_size=100
|
||||||
|
|
||||||
|
Otherwise defaults to using `self.paginate_by`.
|
||||||
|
"""
|
||||||
if "HTTP_X_DISABLE_PAGINATION" in self.request.META:
|
if "HTTP_X_DISABLE_PAGINATION" in self.request.META:
|
||||||
return None
|
return None
|
||||||
return super().get_paginate_by(*args, **kwargs)
|
|
||||||
|
|
||||||
|
if queryset is not None:
|
||||||
|
warnings.warn('The `queryset` parameter to `get_paginate_by()` '
|
||||||
|
'is due to be deprecated.',
|
||||||
|
PendingDeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
|
if self.paginate_by_param:
|
||||||
|
try:
|
||||||
|
return strict_positive_int(
|
||||||
|
self.request.QUERY_PARAMS[self.paginate_by_param],
|
||||||
|
cutoff=self.max_paginate_by
|
||||||
|
)
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.paginate_by
|
||||||
|
|
||||||
class HeadersPaginationMixin(object):
|
|
||||||
def paginate_queryset(self, queryset, page_size=None):
|
def paginate_queryset(self, queryset, page_size=None):
|
||||||
page = super().paginate_queryset(queryset=queryset, page_size=page_size)
|
"""
|
||||||
|
Paginate a queryset if required, either returning a page object,
|
||||||
|
or `None` if pagination is not configured for this view.
|
||||||
|
"""
|
||||||
|
deprecated_style = False
|
||||||
|
if page_size is not None:
|
||||||
|
warnings.warn('The `page_size` parameter to `paginate_queryset()` '
|
||||||
|
'is due to be deprecated. '
|
||||||
|
'Note that the return style of this method is also '
|
||||||
|
'changed, and will simply return a page object '
|
||||||
|
'when called without a `page_size` argument.',
|
||||||
|
PendingDeprecationWarning, stacklevel=2)
|
||||||
|
deprecated_style = True
|
||||||
|
else:
|
||||||
|
# Determine the required page size.
|
||||||
|
# If pagination is not configured, simply return None.
|
||||||
|
page_size = self.get_paginate_by()
|
||||||
|
if not page_size:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self.allow_empty:
|
||||||
|
warnings.warn(
|
||||||
|
'The `allow_empty` parameter is due to be deprecated. '
|
||||||
|
'To use `allow_empty=False` style behavior, You should override '
|
||||||
|
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
|
||||||
|
PendingDeprecationWarning, stacklevel=2
|
||||||
|
)
|
||||||
|
|
||||||
|
paginator = self.paginator_class(queryset, page_size,
|
||||||
|
allow_empty_first_page=self.allow_empty)
|
||||||
|
page_kwarg = self.kwargs.get(self.page_kwarg)
|
||||||
|
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
|
||||||
|
page = page_kwarg or page_query_param or 1
|
||||||
|
try:
|
||||||
|
page_number = paginator.validate_number(page)
|
||||||
|
except InvalidPage:
|
||||||
|
if page == 'last':
|
||||||
|
page_number = paginator.num_pages
|
||||||
|
else:
|
||||||
|
raise Http404(_("Page is not 'last', nor can it be converted to an int."))
|
||||||
|
try:
|
||||||
|
page = paginator.page(page_number)
|
||||||
|
except InvalidPage as e:
|
||||||
|
raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
|
||||||
|
'page_number': page_number,
|
||||||
|
'message': str(e)
|
||||||
|
})
|
||||||
|
|
||||||
if page is None:
|
if page is None:
|
||||||
return page
|
return page
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Parsers are used to parse the content of incoming HTTP requests.
|
||||||
|
|
||||||
|
They give us a generic way of being able to handle various media types
|
||||||
|
on the request, such as form content or json encoded data.
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.uploadhandler import StopFutureHandlers
|
||||||
|
from django.http import QueryDict
|
||||||
|
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
|
||||||
|
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
|
||||||
|
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from taiga.base.exceptions import ParseError
|
||||||
|
from taiga.base.api import renderers
|
||||||
|
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
import decimal
|
||||||
|
|
||||||
|
|
||||||
|
class DataAndFiles(object):
|
||||||
|
def __init__(self, data, files):
|
||||||
|
self.data = data
|
||||||
|
self.files = files
|
||||||
|
|
||||||
|
|
||||||
|
class BaseParser(object):
|
||||||
|
"""
|
||||||
|
All parsers should extend `BaseParser`, specifying a `media_type`
|
||||||
|
attribute, and overriding the `.parse()` method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = None
|
||||||
|
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
"""
|
||||||
|
Given a stream to read from, return the parsed representation.
|
||||||
|
Should return parsed data, or a `DataAndFiles` object consisting of the
|
||||||
|
parsed data and files.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(".parse() must be overridden.")
|
||||||
|
|
||||||
|
|
||||||
|
class JSONParser(BaseParser):
|
||||||
|
"""
|
||||||
|
Parses JSON-serialized data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "application/json"
|
||||||
|
renderer_class = renderers.UnicodeJSONRenderer
|
||||||
|
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
"""
|
||||||
|
Parses the incoming bytestream as JSON and returns the resulting data.
|
||||||
|
"""
|
||||||
|
parser_context = parser_context or {}
|
||||||
|
encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = stream.read().decode(encoding)
|
||||||
|
return json.loads(data)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ParseError("JSON parse error - %s" % six.text_type(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FormParser(BaseParser):
|
||||||
|
"""
|
||||||
|
Parser for form data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
"""
|
||||||
|
Parses the incoming bytestream as a URL encoded form,
|
||||||
|
and returns the resulting QueryDict.
|
||||||
|
"""
|
||||||
|
parser_context = parser_context or {}
|
||||||
|
encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
|
||||||
|
data = QueryDict(stream.read(), encoding=encoding)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class MultiPartParser(BaseParser):
|
||||||
|
"""
|
||||||
|
Parser for multipart form data, which may include file data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "multipart/form-data"
|
||||||
|
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
"""
|
||||||
|
Parses the incoming bytestream as a multipart encoded form,
|
||||||
|
and returns a DataAndFiles object.
|
||||||
|
|
||||||
|
`.data` will be a `QueryDict` containing all the form parameters.
|
||||||
|
`.files` will be a `QueryDict` containing all the form files.
|
||||||
|
"""
|
||||||
|
parser_context = parser_context or {}
|
||||||
|
request = parser_context["request"]
|
||||||
|
encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
|
||||||
|
meta = request.META.copy()
|
||||||
|
meta["CONTENT_TYPE"] = media_type
|
||||||
|
upload_handlers = request.upload_handlers
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding)
|
||||||
|
data, files = parser.parse()
|
||||||
|
return DataAndFiles(data, files)
|
||||||
|
except MultiPartParserError as exc:
|
||||||
|
raise ParseError("Multipart form parse error - %s" % str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadParser(BaseParser):
|
||||||
|
"""
|
||||||
|
Parser for file upload data.
|
||||||
|
"""
|
||||||
|
media_type = "*/*"
|
||||||
|
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None):
|
||||||
|
"""
|
||||||
|
Treats the incoming bytestream as a raw file upload and returns
|
||||||
|
a `DateAndFiles` object.
|
||||||
|
|
||||||
|
`.data` will be None (we expect request body to be a file content).
|
||||||
|
`.files` will be a `QueryDict` containing one "file" element.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parser_context = parser_context or {}
|
||||||
|
request = parser_context["request"]
|
||||||
|
encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
|
||||||
|
meta = request.META
|
||||||
|
upload_handlers = request.upload_handlers
|
||||||
|
filename = self.get_filename(stream, media_type, parser_context)
|
||||||
|
|
||||||
|
# Note that this code is extracted from Django's handling of
|
||||||
|
# file uploads in MultiPartParser.
|
||||||
|
content_type = meta.get("HTTP_CONTENT_TYPE",
|
||||||
|
meta.get("CONTENT_TYPE", ""))
|
||||||
|
try:
|
||||||
|
content_length = int(meta.get("HTTP_CONTENT_LENGTH",
|
||||||
|
meta.get("CONTENT_LENGTH", 0)))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
content_length = None
|
||||||
|
|
||||||
|
# See if the handler will want to take care of the parsing.
|
||||||
|
for handler in upload_handlers:
|
||||||
|
result = handler.handle_raw_input(None,
|
||||||
|
meta,
|
||||||
|
content_length,
|
||||||
|
None,
|
||||||
|
encoding)
|
||||||
|
if result is not None:
|
||||||
|
return DataAndFiles(None, {"file": result[1]})
|
||||||
|
|
||||||
|
# This is the standard case.
|
||||||
|
possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
|
||||||
|
chunk_size = min([2 ** 31 - 4] + possible_sizes)
|
||||||
|
chunks = ChunkIter(stream, chunk_size)
|
||||||
|
counters = [0] * len(upload_handlers)
|
||||||
|
|
||||||
|
for handler in upload_handlers:
|
||||||
|
try:
|
||||||
|
handler.new_file(None, filename, content_type,
|
||||||
|
content_length, encoding)
|
||||||
|
except StopFutureHandlers:
|
||||||
|
break
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
for i, handler in enumerate(upload_handlers):
|
||||||
|
chunk_length = len(chunk)
|
||||||
|
chunk = handler.receive_data_chunk(chunk, counters[i])
|
||||||
|
counters[i] += chunk_length
|
||||||
|
if chunk is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
for i, handler in enumerate(upload_handlers):
|
||||||
|
file_obj = handler.file_complete(counters[i])
|
||||||
|
if file_obj:
|
||||||
|
return DataAndFiles(None, {"file": file_obj})
|
||||||
|
raise ParseError("FileUpload parse error - "
|
||||||
|
"none of upload handlers can handle the stream")
|
||||||
|
|
||||||
|
def get_filename(self, stream, media_type, parser_context):
|
||||||
|
"""
|
||||||
|
Detects the uploaded file name. First searches a "filename" url kwarg.
|
||||||
|
Then tries to parse Content-Disposition header.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return parser_context["kwargs"]["filename"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
meta = parser_context["request"].META
|
||||||
|
disposition = parse_header(meta["HTTP_CONTENT_DISPOSITION"])
|
||||||
|
return disposition[1]["filename"]
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
|
@ -0,0 +1,628 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Serializer fields that deal with relationships.
|
||||||
|
|
||||||
|
These fields allow you to specify the style that should be used to represent
|
||||||
|
model relationships, including hyperlinks, primary keys, or slugs.
|
||||||
|
"""
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
|
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||||
|
from django import forms
|
||||||
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
|
from django.forms import widgets
|
||||||
|
from django.forms.models import ModelChoiceIterator
|
||||||
|
from django.utils.encoding import smart_text
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .fields import Field, WritableField, get_component, is_simple_callable
|
||||||
|
from .reverse import reverse
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##### Relational fields #####
|
||||||
|
|
||||||
|
|
||||||
|
# Not actually Writable, but subclasses may need to be.
|
||||||
|
class RelatedField(WritableField):
|
||||||
|
"""
|
||||||
|
Base class for related model fields.
|
||||||
|
|
||||||
|
This represents a relationship using the unicode representation of the target.
|
||||||
|
"""
|
||||||
|
widget = widgets.Select
|
||||||
|
many_widget = widgets.SelectMultiple
|
||||||
|
form_field_class = forms.ChoiceField
|
||||||
|
many_form_field_class = forms.MultipleChoiceField
|
||||||
|
null_values = (None, "", "None")
|
||||||
|
|
||||||
|
cache_choices = False
|
||||||
|
empty_label = None
|
||||||
|
read_only = True
|
||||||
|
many = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# "null" is to be deprecated in favor of "required"
|
||||||
|
if "null" in kwargs:
|
||||||
|
warnings.warn("The `null` keyword argument is deprecated. "
|
||||||
|
"Use the `required` keyword argument instead.",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
kwargs["required"] = not kwargs.pop("null")
|
||||||
|
|
||||||
|
queryset = kwargs.pop("queryset", None)
|
||||||
|
self.many = kwargs.pop("many", self.many)
|
||||||
|
if self.many:
|
||||||
|
self.widget = self.many_widget
|
||||||
|
self.form_field_class = self.many_form_field_class
|
||||||
|
|
||||||
|
kwargs["read_only"] = kwargs.pop("read_only", self.read_only)
|
||||||
|
super(RelatedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not self.required:
|
||||||
|
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||||
|
|
||||||
|
self.queryset = queryset
|
||||||
|
|
||||||
|
def initialize(self, parent, field_name):
|
||||||
|
super(RelatedField, self).initialize(parent, field_name)
|
||||||
|
if self.queryset is None and not self.read_only:
|
||||||
|
manager = getattr(self.parent.opts.model, self.source or field_name)
|
||||||
|
if hasattr(manager, "related"): # Forward
|
||||||
|
self.queryset = manager.related.model._default_manager.all()
|
||||||
|
else: # Reverse
|
||||||
|
self.queryset = manager.field.rel.to._default_manager.all()
|
||||||
|
|
||||||
|
### We need this stuff to make form choices work...
|
||||||
|
|
||||||
|
def prepare_value(self, obj):
|
||||||
|
return self.to_native(obj)
|
||||||
|
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
"""
|
||||||
|
Return a readable representation for use with eg. select widgets.
|
||||||
|
"""
|
||||||
|
desc = smart_text(obj)
|
||||||
|
ident = smart_text(self.to_native(obj))
|
||||||
|
if desc == ident:
|
||||||
|
return desc
|
||||||
|
return "%s - %s" % (desc, ident)
|
||||||
|
|
||||||
|
def _get_queryset(self):
|
||||||
|
return self._queryset
|
||||||
|
|
||||||
|
def _set_queryset(self, queryset):
|
||||||
|
self._queryset = queryset
|
||||||
|
self.widget.choices = self.choices
|
||||||
|
|
||||||
|
queryset = property(_get_queryset, _set_queryset)
|
||||||
|
|
||||||
|
def _get_choices(self):
|
||||||
|
# If self._choices is set, then somebody must have manually set
|
||||||
|
# the property self.choices. In this case, just return self._choices.
|
||||||
|
if hasattr(self, "_choices"):
|
||||||
|
return self._choices
|
||||||
|
|
||||||
|
# Otherwise, execute the QuerySet in self.queryset to determine the
|
||||||
|
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
|
||||||
|
# consumed. Note that we"re instantiating a new ModelChoiceIterator *each*
|
||||||
|
# time _get_choices() is called (and, thus, each time self.choices is
|
||||||
|
# accessed) so that we can ensure the QuerySet has not been consumed. This
|
||||||
|
# construct might look complicated but it allows for lazy evaluation of
|
||||||
|
# the queryset.
|
||||||
|
return ModelChoiceIterator(self)
|
||||||
|
|
||||||
|
def _set_choices(self, value):
|
||||||
|
# Setting choices also sets the choices on the widget.
|
||||||
|
# choices can be any iterable, but we call list() on it because
|
||||||
|
# it will be consumed more than once.
|
||||||
|
self._choices = self.widget.choices = list(value)
|
||||||
|
|
||||||
|
choices = property(_get_choices, _set_choices)
|
||||||
|
|
||||||
|
### Default value handling
|
||||||
|
|
||||||
|
def get_default_value(self):
|
||||||
|
default = super(RelatedField, self).get_default_value()
|
||||||
|
if self.many and default is None:
|
||||||
|
return []
|
||||||
|
return default
|
||||||
|
|
||||||
|
### Regular serializer stuff...
|
||||||
|
|
||||||
|
def field_to_native(self, obj, field_name):
|
||||||
|
try:
|
||||||
|
if self.source == "*":
|
||||||
|
return self.to_native(obj)
|
||||||
|
|
||||||
|
source = self.source or field_name
|
||||||
|
value = obj
|
||||||
|
|
||||||
|
for component in source.split("."):
|
||||||
|
if value is None:
|
||||||
|
break
|
||||||
|
value = get_component(value, component)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.many:
|
||||||
|
if is_simple_callable(getattr(value, "all", None)):
|
||||||
|
return [self.to_native(item) for item in value.all()]
|
||||||
|
else:
|
||||||
|
# Also support non-queryset iterables.
|
||||||
|
# This allows us to also support plain lists of related items.
|
||||||
|
return [self.to_native(item) for item in value]
|
||||||
|
return self.to_native(value)
|
||||||
|
|
||||||
|
def field_from_native(self, data, files, field_name, into):
|
||||||
|
if self.read_only:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.many:
|
||||||
|
try:
|
||||||
|
# Form data
|
||||||
|
value = data.getlist(field_name)
|
||||||
|
if value == [""] or value == []:
|
||||||
|
raise KeyError
|
||||||
|
except AttributeError:
|
||||||
|
# Non-form data
|
||||||
|
value = data[field_name]
|
||||||
|
else:
|
||||||
|
value = data[field_name]
|
||||||
|
except KeyError:
|
||||||
|
if self.partial:
|
||||||
|
return
|
||||||
|
value = self.get_default_value()
|
||||||
|
|
||||||
|
if value in self.null_values:
|
||||||
|
if self.required:
|
||||||
|
raise ValidationError(self.error_messages["required"])
|
||||||
|
into[(self.source or field_name)] = None
|
||||||
|
elif self.many:
|
||||||
|
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||||
|
else:
|
||||||
|
into[(self.source or field_name)] = self.from_native(value)
|
||||||
|
|
||||||
|
|
||||||
|
### PrimaryKey relationships
|
||||||
|
|
||||||
|
class PrimaryKeyRelatedField(RelatedField):
|
||||||
|
"""
|
||||||
|
Represents a relationship as a pk value.
|
||||||
|
"""
|
||||||
|
read_only = False
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
"does_not_exist": _("Invalid pk '%s' - object does not exist."),
|
||||||
|
"incorrect_type": _("Incorrect type. Expected pk value, received %s."),
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: Remove these field hacks...
|
||||||
|
def prepare_value(self, obj):
|
||||||
|
return self.to_native(obj.pk)
|
||||||
|
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
"""
|
||||||
|
Return a readable representation for use with eg. select widgets.
|
||||||
|
"""
|
||||||
|
desc = smart_text(obj)
|
||||||
|
ident = smart_text(self.to_native(obj.pk))
|
||||||
|
if desc == ident:
|
||||||
|
return desc
|
||||||
|
return "%s - %s" % (desc, ident)
|
||||||
|
|
||||||
|
# TODO: Possibly change this to just take `obj`, through prob less performant
|
||||||
|
def to_native(self, pk):
|
||||||
|
return pk
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
if self.queryset is None:
|
||||||
|
raise Exception("Writable related fields must include a `queryset` argument")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.queryset.get(pk=data)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
msg = self.error_messages["does_not_exist"] % smart_text(data)
|
||||||
|
raise ValidationError(msg)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
received = type(data).__name__
|
||||||
|
msg = self.error_messages["incorrect_type"] % received
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
def field_to_native(self, obj, field_name):
|
||||||
|
if self.many:
|
||||||
|
# To-many relationship
|
||||||
|
|
||||||
|
queryset = None
|
||||||
|
if not self.source:
|
||||||
|
# Prefer obj.serializable_value for performance reasons
|
||||||
|
try:
|
||||||
|
queryset = obj.serializable_value(field_name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if queryset is None:
|
||||||
|
# RelatedManager (reverse relationship)
|
||||||
|
source = self.source or field_name
|
||||||
|
queryset = obj
|
||||||
|
for component in source.split("."):
|
||||||
|
if queryset is None:
|
||||||
|
return []
|
||||||
|
queryset = get_component(queryset, component)
|
||||||
|
|
||||||
|
# Forward relationship
|
||||||
|
if is_simple_callable(getattr(queryset, "all", None)):
|
||||||
|
return [self.to_native(item.pk) for item in queryset.all()]
|
||||||
|
else:
|
||||||
|
# Also support non-queryset iterables.
|
||||||
|
# This allows us to also support plain lists of related items.
|
||||||
|
return [self.to_native(item.pk) for item in queryset]
|
||||||
|
|
||||||
|
# To-one relationship
|
||||||
|
try:
|
||||||
|
# Prefer obj.serializable_value for performance reasons
|
||||||
|
pk = obj.serializable_value(self.source or field_name)
|
||||||
|
except AttributeError:
|
||||||
|
# RelatedObject (reverse relationship)
|
||||||
|
try:
|
||||||
|
pk = getattr(obj, self.source or field_name).pk
|
||||||
|
except (ObjectDoesNotExist, AttributeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Forward relationship
|
||||||
|
return self.to_native(pk)
|
||||||
|
|
||||||
|
|
||||||
|
### Slug relationships
|
||||||
|
|
||||||
|
|
||||||
|
class SlugRelatedField(RelatedField):
|
||||||
|
"""
|
||||||
|
Represents a relationship using a unique field on the target.
|
||||||
|
"""
|
||||||
|
read_only = False
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
"does_not_exist": _("Object with %s=%s does not exist."),
|
||||||
|
"invalid": _("Invalid value."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.slug_field = kwargs.pop("slug_field", None)
|
||||||
|
assert self.slug_field, "slug_field is required"
|
||||||
|
super(SlugRelatedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
return getattr(obj, self.slug_field)
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
if self.queryset is None:
|
||||||
|
raise Exception("Writable related fields must include a `queryset` argument")
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.queryset.get(**{self.slug_field: data})
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError(self.error_messages["does_not_exist"] %
|
||||||
|
(self.slug_field, smart_text(data)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
msg = self.error_messages["invalid"]
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
### Hyperlinked relationships
|
||||||
|
|
||||||
|
class HyperlinkedRelatedField(RelatedField):
|
||||||
|
"""
|
||||||
|
Represents a relationship using hyperlinking.
|
||||||
|
"""
|
||||||
|
read_only = False
|
||||||
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
"no_match": _("Invalid hyperlink - No URL match"),
|
||||||
|
"incorrect_match": _("Invalid hyperlink - Incorrect URL match"),
|
||||||
|
"configuration_error": _("Invalid hyperlink due to configuration error"),
|
||||||
|
"does_not_exist": _("Invalid hyperlink - object does not exist."),
|
||||||
|
"incorrect_type": _("Incorrect type. Expected url string, received %s."),
|
||||||
|
}
|
||||||
|
|
||||||
|
# These are all pending deprecation
|
||||||
|
pk_url_kwarg = "pk"
|
||||||
|
slug_field = "slug"
|
||||||
|
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.view_name = kwargs.pop("view_name")
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError("Hyperlinked field requires \"view_name\" kwarg")
|
||||||
|
|
||||||
|
self.lookup_field = kwargs.pop("lookup_field", self.lookup_field)
|
||||||
|
self.format = kwargs.pop("format", None)
|
||||||
|
|
||||||
|
# These are pending deprecation
|
||||||
|
if "pk_url_kwarg" in kwargs:
|
||||||
|
msg = "pk_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
if "slug_url_kwarg" in kwargs:
|
||||||
|
msg = "slug_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
if "slug_field" in kwargs:
|
||||||
|
msg = "slug_field is pending deprecation. Use lookup_field instead."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
|
self.pk_url_kwarg = kwargs.pop("pk_url_kwarg", self.pk_url_kwarg)
|
||||||
|
self.slug_field = kwargs.pop("slug_field", self.slug_field)
|
||||||
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||||
|
self.slug_url_kwarg = kwargs.pop("slug_url_kwarg", default_slug_kwarg)
|
||||||
|
|
||||||
|
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_url(self, obj, view_name, request, format):
|
||||||
|
"""
|
||||||
|
Given an object, return the URL that hyperlinks to the object.
|
||||||
|
|
||||||
|
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||||
|
attributes are not configured to correctly match the URL conf.
|
||||||
|
"""
|
||||||
|
lookup_field = getattr(obj, self.lookup_field)
|
||||||
|
kwargs = {self.lookup_field: lookup_field}
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.pk_url_kwarg != "pk":
|
||||||
|
# Only try pk if it has been explicitly set.
|
||||||
|
# Otherwise, the default `lookup_field = "pk"` has us covered.
|
||||||
|
pk = obj.pk
|
||||||
|
kwargs = {self.pk_url_kwarg: pk}
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
slug = getattr(obj, self.slug_field, None)
|
||||||
|
if slug is not None:
|
||||||
|
# Only try slug if it corresponds to an attribute on the object.
|
||||||
|
kwargs = {self.slug_url_kwarg: slug}
|
||||||
|
try:
|
||||||
|
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
if self.slug_field == "slug" and self.slug_url_kwarg == "slug":
|
||||||
|
# If the lookup succeeds using the default slug params,
|
||||||
|
# then `slug_field` is being used implicitly, and we
|
||||||
|
# we need to warn about the pending deprecation.
|
||||||
|
msg = "Implicit slug field hyperlinked fields are pending deprecation." \
|
||||||
|
"You should set `lookup_field=slug` on the HyperlinkedRelatedField."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
return ret
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise NoReverseMatch()
|
||||||
|
|
||||||
|
def get_object(self, queryset, view_name, view_args, view_kwargs):
|
||||||
|
"""
|
||||||
|
Return the object corresponding to a matched URL.
|
||||||
|
|
||||||
|
Takes the matched URL conf arguments, and the queryset, and should
|
||||||
|
return an object instance, or raise an `ObjectDoesNotExist` exception.
|
||||||
|
"""
|
||||||
|
lookup = view_kwargs.get(self.lookup_field, None)
|
||||||
|
pk = view_kwargs.get(self.pk_url_kwarg, None)
|
||||||
|
slug = view_kwargs.get(self.slug_url_kwarg, None)
|
||||||
|
|
||||||
|
if lookup is not None:
|
||||||
|
filter_kwargs = {self.lookup_field: lookup}
|
||||||
|
elif pk is not None:
|
||||||
|
filter_kwargs = {"pk": pk}
|
||||||
|
elif slug is not None:
|
||||||
|
filter_kwargs = {self.slug_field: slug}
|
||||||
|
else:
|
||||||
|
raise ObjectDoesNotExist()
|
||||||
|
|
||||||
|
return queryset.get(**filter_kwargs)
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
view_name = self.view_name
|
||||||
|
request = self.context.get("request", None)
|
||||||
|
format = self.format or self.context.get("format", None)
|
||||||
|
|
||||||
|
if request is None:
|
||||||
|
msg = (
|
||||||
|
"Using `HyperlinkedRelatedField` without including the request "
|
||||||
|
"in the serializer context is deprecated. "
|
||||||
|
"Add `context={'request': request}` when instantiating "
|
||||||
|
"the serializer."
|
||||||
|
)
|
||||||
|
warnings.warn(msg, DeprecationWarning, stacklevel=4)
|
||||||
|
|
||||||
|
# If the object has not yet been saved then we cannot hyperlink to it.
|
||||||
|
if getattr(obj, "pk", None) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Return the hyperlink, or error if incorrectly configured.
|
||||||
|
try:
|
||||||
|
return self.get_url(obj, view_name, request, format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
msg = (
|
||||||
|
"Could not resolve URL for hyperlinked relationship using "
|
||||||
|
"view name '%s'. You may have failed to include the related "
|
||||||
|
"model in your API, or incorrectly configured the "
|
||||||
|
"`lookup_field` attribute on this field."
|
||||||
|
)
|
||||||
|
raise Exception(msg % view_name)
|
||||||
|
|
||||||
|
def from_native(self, value):
|
||||||
|
# Convert URL -> model instance pk
|
||||||
|
# TODO: Use values_list
|
||||||
|
queryset = self.queryset
|
||||||
|
if queryset is None:
|
||||||
|
raise Exception("Writable related fields must include a `queryset` argument")
|
||||||
|
|
||||||
|
try:
|
||||||
|
http_prefix = value.startswith(("http:", "https:"))
|
||||||
|
except AttributeError:
|
||||||
|
msg = self.error_messages["incorrect_type"]
|
||||||
|
raise ValidationError(msg % type(value).__name__)
|
||||||
|
|
||||||
|
if http_prefix:
|
||||||
|
# If needed convert absolute URLs to relative path
|
||||||
|
value = urlparse.urlparse(value).path
|
||||||
|
prefix = get_script_prefix()
|
||||||
|
if value.startswith(prefix):
|
||||||
|
value = "/" + value[len(prefix):]
|
||||||
|
|
||||||
|
try:
|
||||||
|
match = resolve(value)
|
||||||
|
except Exception:
|
||||||
|
raise ValidationError(self.error_messages["no_match"])
|
||||||
|
|
||||||
|
if match.view_name != self.view_name:
|
||||||
|
raise ValidationError(self.error_messages["incorrect_match"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.get_object(queryset, match.view_name,
|
||||||
|
match.args, match.kwargs)
|
||||||
|
except (ObjectDoesNotExist, TypeError, ValueError):
|
||||||
|
raise ValidationError(self.error_messages["does_not_exist"])
|
||||||
|
|
||||||
|
|
||||||
|
class HyperlinkedIdentityField(Field):
|
||||||
|
"""
|
||||||
|
Represents the instance, or a property on the instance, using hyperlinking.
|
||||||
|
"""
|
||||||
|
lookup_field = "pk"
|
||||||
|
read_only = True
|
||||||
|
|
||||||
|
# These are all pending deprecation
|
||||||
|
pk_url_kwarg = "pk"
|
||||||
|
slug_field = "slug"
|
||||||
|
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
self.view_name = kwargs.pop("view_name")
|
||||||
|
except KeyError:
|
||||||
|
msg = "HyperlinkedIdentityField requires \"view_name\" argument"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
self.format = kwargs.pop("format", None)
|
||||||
|
lookup_field = kwargs.pop("lookup_field", None)
|
||||||
|
self.lookup_field = lookup_field or self.lookup_field
|
||||||
|
|
||||||
|
# These are pending deprecation
|
||||||
|
if "pk_url_kwarg" in kwargs:
|
||||||
|
msg = "pk_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
if "slug_url_kwarg" in kwargs:
|
||||||
|
msg = "slug_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
if "slug_field" in kwargs:
|
||||||
|
msg = "slug_field is pending deprecation. Use lookup_field instead."
|
||||||
|
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
|
self.slug_field = kwargs.pop("slug_field", self.slug_field)
|
||||||
|
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||||
|
self.pk_url_kwarg = kwargs.pop("pk_url_kwarg", self.pk_url_kwarg)
|
||||||
|
self.slug_url_kwarg = kwargs.pop("slug_url_kwarg", default_slug_kwarg)
|
||||||
|
|
||||||
|
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def field_to_native(self, obj, field_name):
|
||||||
|
request = self.context.get("request", None)
|
||||||
|
format = self.context.get("format", None)
|
||||||
|
view_name = self.view_name
|
||||||
|
|
||||||
|
if request is None:
|
||||||
|
warnings.warn("Using `HyperlinkedIdentityField` without including the "
|
||||||
|
"request in the serializer context is deprecated. "
|
||||||
|
"Add `context={'request': request}` when instantiating the serializer.",
|
||||||
|
DeprecationWarning, stacklevel=4)
|
||||||
|
|
||||||
|
# By default use whatever format is given for the current context
|
||||||
|
# unless the target is a different type to the source.
|
||||||
|
#
|
||||||
|
# Eg. Consider a HyperlinkedIdentityField pointing from a json
|
||||||
|
# representation to an html property of that representation...
|
||||||
|
#
|
||||||
|
# "/snippets/1/" should link to "/snippets/1/highlight/"
|
||||||
|
# ...but...
|
||||||
|
# "/snippets/1/.json" should link to "/snippets/1/highlight/.html"
|
||||||
|
if format and self.format and self.format != format:
|
||||||
|
format = self.format
|
||||||
|
|
||||||
|
# Return the hyperlink, or error if incorrectly configured.
|
||||||
|
try:
|
||||||
|
return self.get_url(obj, view_name, request, format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
msg = (
|
||||||
|
"Could not resolve URL for hyperlinked relationship using "
|
||||||
|
"view name '%s'. You may have failed to include the related "
|
||||||
|
"model in your API, or incorrectly configured the "
|
||||||
|
"`lookup_field` attribute on this field."
|
||||||
|
)
|
||||||
|
raise Exception(msg % view_name)
|
||||||
|
|
||||||
|
def get_url(self, obj, view_name, request, format):
|
||||||
|
"""
|
||||||
|
Given an object, return the URL that hyperlinks to the object.
|
||||||
|
|
||||||
|
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||||
|
attributes are not configured to correctly match the URL conf.
|
||||||
|
"""
|
||||||
|
lookup_field = getattr(obj, self.lookup_field, None)
|
||||||
|
kwargs = {self.lookup_field: lookup_field}
|
||||||
|
|
||||||
|
# Handle unsaved object case
|
||||||
|
if lookup_field is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.pk_url_kwarg != "pk":
|
||||||
|
# Only try pk lookup if it has been explicitly set.
|
||||||
|
# Otherwise, the default `lookup_field = "pk"` has us covered.
|
||||||
|
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
slug = getattr(obj, self.slug_field, None)
|
||||||
|
if slug:
|
||||||
|
# Only use slug lookup if a slug field exists on the model
|
||||||
|
kwargs = {self.slug_url_kwarg: slug}
|
||||||
|
try:
|
||||||
|
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
except NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise NoReverseMatch()
|
|
@ -0,0 +1,613 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Renderers are used to serialize a response into specific media types.
|
||||||
|
|
||||||
|
They give us a generic way of being able to handle various media types
|
||||||
|
on the response, such as JSON encoded data or HTML output.
|
||||||
|
|
||||||
|
REST framework also provides an HTML renderer the renders the browsable API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.http.multipartparser import parse_header
|
||||||
|
from django.template import RequestContext, loader, Template
|
||||||
|
from django.test.client import encode_multipart
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.encoding import smart_text
|
||||||
|
from django.utils.six import StringIO
|
||||||
|
from django.utils.xmlutils import SimplerXMLGenerator
|
||||||
|
|
||||||
|
from taiga.base import exceptions, status
|
||||||
|
from taiga.base.exceptions import ParseError
|
||||||
|
|
||||||
|
from . import VERSION
|
||||||
|
from .request import is_form_media_type, override_method
|
||||||
|
from .settings import api_settings
|
||||||
|
from .utils import encoders
|
||||||
|
from .utils.breadcrumbs import get_breadcrumbs
|
||||||
|
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRenderer(object):
|
||||||
|
"""
|
||||||
|
All renderers should extend this class, setting the `media_type`
|
||||||
|
and `format` attributes, and override the `.render()` method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = None
|
||||||
|
format = None
|
||||||
|
charset = "utf-8"
|
||||||
|
render_style = "text"
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
raise NotImplemented("Renderer class requires .render() to be implemented")
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRenderer(BaseRenderer):
|
||||||
|
"""
|
||||||
|
Renderer which serializes to JSON.
|
||||||
|
Applies JSON's backslash-u character escaping for non-ascii characters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "application/json"
|
||||||
|
format = "json"
|
||||||
|
encoder_class = encoders.JSONEncoder
|
||||||
|
ensure_ascii = True
|
||||||
|
charset = None
|
||||||
|
# JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32.
|
||||||
|
# See: http://www.ietf.org/rfc/rfc4627.txt
|
||||||
|
# Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
"""
|
||||||
|
Render `data` into JSON.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
return bytes()
|
||||||
|
|
||||||
|
# If "indent" is provided in the context, then pretty print the result.
|
||||||
|
# E.g. If we"re being called by the BrowsableAPIRenderer.
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
indent = renderer_context.get("indent", None)
|
||||||
|
|
||||||
|
if accepted_media_type:
|
||||||
|
# If the media type looks like "application/json; indent=4",
|
||||||
|
# then pretty print the result.
|
||||||
|
base_media_type, params = parse_header(accepted_media_type.encode("ascii"))
|
||||||
|
indent = params.get("indent", indent)
|
||||||
|
try:
|
||||||
|
indent = max(min(int(indent), 8), 0)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
indent = None
|
||||||
|
|
||||||
|
ret = json.dumps(data, cls=self.encoder_class,
|
||||||
|
indent=indent, ensure_ascii=self.ensure_ascii)
|
||||||
|
|
||||||
|
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
|
||||||
|
# but if ensure_ascii=False, the return type is underspecified,
|
||||||
|
# and may (or may not) be unicode.
|
||||||
|
# On python 3.x json.dumps() returns unicode strings.
|
||||||
|
if isinstance(ret, six.text_type):
|
||||||
|
return bytes(ret.encode("utf-8"))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class UnicodeJSONRenderer(JSONRenderer):
|
||||||
|
ensure_ascii = False
|
||||||
|
"""
|
||||||
|
Renderer which serializes to JSON.
|
||||||
|
Does *not* apply JSON's character escaping for non-ascii characters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class JSONPRenderer(JSONRenderer):
|
||||||
|
"""
|
||||||
|
Renderer which serializes to json,
|
||||||
|
wrapping the json output in a callback function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "application/javascript"
|
||||||
|
format = "jsonp"
|
||||||
|
callback_parameter = "callback"
|
||||||
|
default_callback = "callback"
|
||||||
|
charset = "utf-8"
|
||||||
|
|
||||||
|
def get_callback(self, renderer_context):
|
||||||
|
"""
|
||||||
|
Determine the name of the callback to wrap around the json output.
|
||||||
|
"""
|
||||||
|
request = renderer_context.get("request", None)
|
||||||
|
params = request and request.QUERY_PARAMS or {}
|
||||||
|
return params.get(self.callback_parameter, self.default_callback)
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
"""
|
||||||
|
Renders into jsonp, wrapping the json output in a callback function.
|
||||||
|
|
||||||
|
Clients may set the callback function name using a query parameter
|
||||||
|
on the URL, for example: ?callback=exampleCallbackName
|
||||||
|
"""
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
callback = self.get_callback(renderer_context)
|
||||||
|
json = super(JSONPRenderer, self).render(data, accepted_media_type,
|
||||||
|
renderer_context)
|
||||||
|
return callback.encode(self.charset) + b"(" + json + b");"
|
||||||
|
|
||||||
|
|
||||||
|
class XMLRenderer(BaseRenderer):
|
||||||
|
"""
|
||||||
|
Renderer which serializes to XML.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "application/xml"
|
||||||
|
format = "xml"
|
||||||
|
charset = "utf-8"
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
"""
|
||||||
|
Renders `data` into serialized XML.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
stream = StringIO()
|
||||||
|
|
||||||
|
xml = SimplerXMLGenerator(stream, self.charset)
|
||||||
|
xml.startDocument()
|
||||||
|
xml.startElement("root", {})
|
||||||
|
|
||||||
|
self._to_xml(xml, data)
|
||||||
|
|
||||||
|
xml.endElement("root")
|
||||||
|
xml.endDocument()
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
def _to_xml(self, xml, data):
|
||||||
|
if isinstance(data, (list, tuple)):
|
||||||
|
for item in data:
|
||||||
|
xml.startElement("list-item", {})
|
||||||
|
self._to_xml(xml, item)
|
||||||
|
xml.endElement("list-item")
|
||||||
|
|
||||||
|
elif isinstance(data, dict):
|
||||||
|
for key, value in six.iteritems(data):
|
||||||
|
xml.startElement(key, {})
|
||||||
|
self._to_xml(xml, value)
|
||||||
|
xml.endElement(key)
|
||||||
|
|
||||||
|
elif data is None:
|
||||||
|
# Don't output any value
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
xml.characters(smart_text(data))
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateHTMLRenderer(BaseRenderer):
|
||||||
|
"""
|
||||||
|
An HTML renderer for use with templates.
|
||||||
|
|
||||||
|
The data supplied to the Response object should be a dictionary that will
|
||||||
|
be used as context for the template.
|
||||||
|
|
||||||
|
The template name is determined by (in order of preference):
|
||||||
|
|
||||||
|
1. An explicit `.template_name` attribute set on the response.
|
||||||
|
2. An explicit `.template_name` attribute set on this class.
|
||||||
|
3. The return result of calling `view.get_template_names()`.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
data = {"users": User.objects.all()}
|
||||||
|
return Response(data, template_name="users.html")
|
||||||
|
|
||||||
|
For pre-rendered HTML, see StaticHTMLRenderer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
media_type = "text/html"
|
||||||
|
format = "html"
|
||||||
|
template_name = None
|
||||||
|
exception_template_names = [
|
||||||
|
"%(status_code)s.html",
|
||||||
|
"api_exception.html"
|
||||||
|
]
|
||||||
|
charset = "utf-8"
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
"""
|
||||||
|
Renders data to HTML, using Django's standard template rendering.
|
||||||
|
|
||||||
|
The template name is determined by (in order of preference):
|
||||||
|
|
||||||
|
1. An explicit .template_name set on the response.
|
||||||
|
2. An explicit .template_name set on this class.
|
||||||
|
3. The return result of calling view.get_template_names().
|
||||||
|
"""
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
view = renderer_context["view"]
|
||||||
|
request = renderer_context["request"]
|
||||||
|
response = renderer_context["response"]
|
||||||
|
|
||||||
|
if response.exception:
|
||||||
|
template = self.get_exception_template(response)
|
||||||
|
else:
|
||||||
|
template_names = self.get_template_names(response, view)
|
||||||
|
template = self.resolve_template(template_names)
|
||||||
|
|
||||||
|
context = self.resolve_context(data, request, response)
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
def resolve_template(self, template_names):
|
||||||
|
return loader.select_template(template_names)
|
||||||
|
|
||||||
|
def resolve_context(self, data, request, response):
|
||||||
|
if response.exception:
|
||||||
|
data["status_code"] = response.status_code
|
||||||
|
return RequestContext(request, data)
|
||||||
|
|
||||||
|
def get_template_names(self, response, view):
|
||||||
|
if response.template_name:
|
||||||
|
return [response.template_name]
|
||||||
|
elif self.template_name:
|
||||||
|
return [self.template_name]
|
||||||
|
elif hasattr(view, "get_template_names"):
|
||||||
|
return view.get_template_names()
|
||||||
|
elif hasattr(view, "template_name"):
|
||||||
|
return [view.template_name]
|
||||||
|
raise ImproperlyConfigured("Returned a template response with no `template_name` attribute set on either the view or response")
|
||||||
|
|
||||||
|
def get_exception_template(self, response):
|
||||||
|
template_names = [name % {"status_code": response.status_code}
|
||||||
|
for name in self.exception_template_names]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to find an appropriate error template
|
||||||
|
return self.resolve_template(template_names)
|
||||||
|
except Exception:
|
||||||
|
# Fall back to using eg "404 Not Found"
|
||||||
|
return Template("%d %s" % (response.status_code,
|
||||||
|
response.status_text.title()))
|
||||||
|
|
||||||
|
|
||||||
|
# Note, subclass TemplateHTMLRenderer simply for the exception behavior
|
||||||
|
class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||||
|
"""
|
||||||
|
An HTML renderer class that simply returns pre-rendered HTML.
|
||||||
|
|
||||||
|
The data supplied to the Response object should be a string representing
|
||||||
|
the pre-rendered HTML content.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
data = "<html><body>example</body></html>"
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
For template rendered HTML, see TemplateHTMLRenderer.
|
||||||
|
"""
|
||||||
|
media_type = "text/html"
|
||||||
|
format = "html"
|
||||||
|
charset = "utf-8"
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
response = renderer_context["response"]
|
||||||
|
|
||||||
|
if response and response.exception:
|
||||||
|
request = renderer_context["request"]
|
||||||
|
template = self.get_exception_template(response)
|
||||||
|
context = self.resolve_context(data, request, response)
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class HTMLFormRenderer(BaseRenderer):
|
||||||
|
"""
|
||||||
|
Renderers serializer data into an HTML form.
|
||||||
|
|
||||||
|
If the serializer was instantiated without an object then this will
|
||||||
|
return an HTML form not bound to any object,
|
||||||
|
otherwise it will return an HTML form with the appropriate initial data
|
||||||
|
populated from the object.
|
||||||
|
|
||||||
|
Note that rendering of field and form errors is not currently supported.
|
||||||
|
"""
|
||||||
|
media_type = "text/html"
|
||||||
|
format = "form"
|
||||||
|
template = "api/form.html"
|
||||||
|
charset = "utf-8"
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
"""
|
||||||
|
Render serializer data and return an HTML form, as a string.
|
||||||
|
"""
|
||||||
|
renderer_context = renderer_context or {}
|
||||||
|
request = renderer_context["request"]
|
||||||
|
|
||||||
|
template = loader.get_template(self.template)
|
||||||
|
context = RequestContext(request, {"form": data})
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
class BrowsableAPIRenderer(BaseRenderer):
|
||||||
|
"""
|
||||||
|
HTML renderer used to self-document the API.
|
||||||
|
"""
|
||||||
|
media_type = "text/html"
|
||||||
|
format = "api"
|
||||||
|
template = "api/api.html"
|
||||||
|
charset = "utf-8"
|
||||||
|
form_renderer_class = HTMLFormRenderer
|
||||||
|
|
||||||
|
def get_default_renderer(self, view):
|
||||||
|
"""
|
||||||
|
Return an instance of the first valid renderer.
|
||||||
|
(Don't use another documenting renderer.)
|
||||||
|
"""
|
||||||
|
renderers = [renderer for renderer in view.renderer_classes
|
||||||
|
if not issubclass(renderer, BrowsableAPIRenderer)]
|
||||||
|
non_template_renderers = [renderer for renderer in renderers
|
||||||
|
if not hasattr(renderer, "get_template_names")]
|
||||||
|
|
||||||
|
if not renderers:
|
||||||
|
return None
|
||||||
|
elif non_template_renderers:
|
||||||
|
return non_template_renderers[0]()
|
||||||
|
return renderers[0]()
|
||||||
|
|
||||||
|
def get_content(self, renderer, data,
|
||||||
|
accepted_media_type, renderer_context):
|
||||||
|
"""
|
||||||
|
Get the content as if it had been rendered by the default
|
||||||
|
non-documenting renderer.
|
||||||
|
"""
|
||||||
|
if not renderer:
|
||||||
|
return "[No renderers were found]"
|
||||||
|
|
||||||
|
renderer_context["indent"] = 4
|
||||||
|
content = renderer.render(data, accepted_media_type, renderer_context)
|
||||||
|
|
||||||
|
render_style = getattr(renderer, "render_style", "text")
|
||||||
|
assert render_style in ["text", "binary"], 'Expected .render_style "text" or "binary", ' \
|
||||||
|
'but got "%s"' % render_style
|
||||||
|
if render_style == "binary":
|
||||||
|
return "[%d bytes of binary content]" % len(content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def show_form_for_method(self, view, method, request, obj):
|
||||||
|
"""
|
||||||
|
Returns True if a form should be shown for this method.
|
||||||
|
"""
|
||||||
|
if not method in view.allowed_methods:
|
||||||
|
return # Not a valid method
|
||||||
|
|
||||||
|
if not api_settings.FORM_METHOD_OVERRIDE:
|
||||||
|
return # Cannot use form overloading
|
||||||
|
|
||||||
|
try:
|
||||||
|
view.check_permissions(request)
|
||||||
|
if obj is not None:
|
||||||
|
view.check_object_permissions(request, obj)
|
||||||
|
except exceptions.APIException:
|
||||||
|
return False # Doesn't have permissions
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_rendered_html_form(self, view, method, request):
|
||||||
|
"""
|
||||||
|
Return a string representing a rendered HTML form, possibly bound to
|
||||||
|
either the input or output data.
|
||||||
|
|
||||||
|
In the absence of the View having an associated form then return None.
|
||||||
|
"""
|
||||||
|
if request.method == method:
|
||||||
|
try:
|
||||||
|
data = request.DATA
|
||||||
|
files = request.FILES
|
||||||
|
except ParseError:
|
||||||
|
data = None
|
||||||
|
files = None
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
files = None
|
||||||
|
|
||||||
|
with override_method(view, request, method) as request:
|
||||||
|
obj = getattr(view, "object", None)
|
||||||
|
if not self.show_form_for_method(view, method, request, obj):
|
||||||
|
return
|
||||||
|
|
||||||
|
if method in ("DELETE", "OPTIONS"):
|
||||||
|
return True # Don't actually need to return a form
|
||||||
|
|
||||||
|
if (not getattr(view, "get_serializer", None)
|
||||||
|
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)):
|
||||||
|
return
|
||||||
|
|
||||||
|
serializer = view.get_serializer(instance=obj, data=data, files=files)
|
||||||
|
serializer.is_valid()
|
||||||
|
data = serializer.data
|
||||||
|
|
||||||
|
form_renderer = self.form_renderer_class()
|
||||||
|
return form_renderer.render(data, self.accepted_media_type, self.renderer_context)
|
||||||
|
|
||||||
|
def get_raw_data_form(self, view, method, request):
|
||||||
|
"""
|
||||||
|
Returns a form that allows for arbitrary content types to be tunneled
|
||||||
|
via standard HTML forms.
|
||||||
|
(Which are typically application/x-www-form-urlencoded)
|
||||||
|
"""
|
||||||
|
with override_method(view, request, method) as request:
|
||||||
|
# If we"re not using content overloading there's no point in
|
||||||
|
# supplying a generic form, as the view won't treat the form"s
|
||||||
|
# value as the content of the request.
|
||||||
|
if not (api_settings.FORM_CONTENT_OVERRIDE
|
||||||
|
and api_settings.FORM_CONTENTTYPE_OVERRIDE):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check permissions
|
||||||
|
obj = getattr(view, "object", None)
|
||||||
|
if not self.show_form_for_method(view, method, request, obj):
|
||||||
|
return
|
||||||
|
|
||||||
|
# If possible, serialize the initial content for the generic form
|
||||||
|
default_parser = view.parser_classes[0]
|
||||||
|
renderer_class = getattr(default_parser, "renderer_class", None)
|
||||||
|
if (hasattr(view, "get_serializer") and renderer_class):
|
||||||
|
# View has a serializer defined and parser class has a
|
||||||
|
# corresponding renderer that can be used to render the data.
|
||||||
|
|
||||||
|
# Get a read-only version of the serializer
|
||||||
|
serializer = view.get_serializer(instance=obj)
|
||||||
|
if obj is None:
|
||||||
|
for name, field in serializer.fields.items():
|
||||||
|
if getattr(field, "read_only", None):
|
||||||
|
del serializer.fields[name]
|
||||||
|
|
||||||
|
# Render the raw data content
|
||||||
|
renderer = renderer_class()
|
||||||
|
accepted = self.accepted_media_type
|
||||||
|
context = self.renderer_context.copy()
|
||||||
|
context["indent"] = 4
|
||||||
|
content = renderer.render(serializer.data, accepted, context)
|
||||||
|
else:
|
||||||
|
content = None
|
||||||
|
|
||||||
|
# Generate a generic form that includes a content type field,
|
||||||
|
# and a content field.
|
||||||
|
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||||
|
content_field = api_settings.FORM_CONTENT_OVERRIDE
|
||||||
|
|
||||||
|
media_types = [parser.media_type for parser in view.parser_classes]
|
||||||
|
choices = [(media_type, media_type) for media_type in media_types]
|
||||||
|
initial = media_types[0]
|
||||||
|
|
||||||
|
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||||
|
class GenericContentForm(forms.Form):
|
||||||
|
def __init__(self):
|
||||||
|
super(GenericContentForm, self).__init__()
|
||||||
|
|
||||||
|
self.fields[content_type_field] = forms.ChoiceField(
|
||||||
|
label="Media type",
|
||||||
|
choices=choices,
|
||||||
|
initial=initial
|
||||||
|
)
|
||||||
|
self.fields[content_field] = forms.CharField(
|
||||||
|
label="Content",
|
||||||
|
widget=forms.Textarea,
|
||||||
|
initial=content
|
||||||
|
)
|
||||||
|
|
||||||
|
return GenericContentForm()
|
||||||
|
|
||||||
|
def get_name(self, view):
|
||||||
|
return view.get_view_name()
|
||||||
|
|
||||||
|
def get_description(self, view):
|
||||||
|
return view.get_view_description(html=True)
|
||||||
|
|
||||||
|
def get_breadcrumbs(self, request):
|
||||||
|
return get_breadcrumbs(request.path)
|
||||||
|
|
||||||
|
def get_context(self, data, accepted_media_type, renderer_context):
|
||||||
|
"""
|
||||||
|
Returns the context used to render.
|
||||||
|
"""
|
||||||
|
view = renderer_context["view"]
|
||||||
|
request = renderer_context["request"]
|
||||||
|
response = renderer_context["response"]
|
||||||
|
|
||||||
|
renderer = self.get_default_renderer(view)
|
||||||
|
|
||||||
|
raw_data_post_form = self.get_raw_data_form(view, "POST", request)
|
||||||
|
raw_data_put_form = self.get_raw_data_form(view, "PUT", request)
|
||||||
|
raw_data_patch_form = self.get_raw_data_form(view, "PATCH", request)
|
||||||
|
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||||
|
|
||||||
|
response_headers = dict(response.items())
|
||||||
|
renderer_content_type = ""
|
||||||
|
if renderer:
|
||||||
|
renderer_content_type = "%s" % renderer.media_type
|
||||||
|
if renderer.charset:
|
||||||
|
renderer_content_type += " ;%s" % renderer.charset
|
||||||
|
response_headers["Content-Type"] = renderer_content_type
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"content": self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||||
|
"view": view,
|
||||||
|
"request": request,
|
||||||
|
"response": response,
|
||||||
|
"description": self.get_description(view),
|
||||||
|
"name": self.get_name(view),
|
||||||
|
"version": VERSION,
|
||||||
|
"breadcrumblist": self.get_breadcrumbs(request),
|
||||||
|
"allowed_methods": view.allowed_methods,
|
||||||
|
"available_formats": [renderer.format for renderer in view.renderer_classes],
|
||||||
|
"response_headers": response_headers,
|
||||||
|
|
||||||
|
"put_form": self.get_rendered_html_form(view, "PUT", request),
|
||||||
|
"post_form": self.get_rendered_html_form(view, "POST", request),
|
||||||
|
"delete_form": self.get_rendered_html_form(view, "DELETE", request),
|
||||||
|
"options_form": self.get_rendered_html_form(view, "OPTIONS", request),
|
||||||
|
|
||||||
|
"raw_data_put_form": raw_data_put_form,
|
||||||
|
"raw_data_post_form": raw_data_post_form,
|
||||||
|
"raw_data_patch_form": raw_data_patch_form,
|
||||||
|
"raw_data_put_or_patch_form": raw_data_put_or_patch_form,
|
||||||
|
|
||||||
|
"display_edit_forms": bool(response.status_code != 403),
|
||||||
|
|
||||||
|
"api_settings": api_settings
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
"""
|
||||||
|
Render the HTML for the browsable API representation.
|
||||||
|
"""
|
||||||
|
self.accepted_media_type = accepted_media_type or ""
|
||||||
|
self.renderer_context = renderer_context or {}
|
||||||
|
|
||||||
|
template = loader.get_template(self.template)
|
||||||
|
context = self.get_context(data, accepted_media_type, renderer_context)
|
||||||
|
context = RequestContext(renderer_context["request"], context)
|
||||||
|
ret = template.render(context)
|
||||||
|
|
||||||
|
# Munge DELETE Response code to allow us to return content
|
||||||
|
# (Do this *after* we"ve rendered the template so that we include
|
||||||
|
# the normal deletion response code in the output)
|
||||||
|
response = renderer_context["response"]
|
||||||
|
if response.status_code == status.HTTP_204_NO_CONTENT:
|
||||||
|
response.status_code = status.HTTP_200_OK
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class MultiPartRenderer(BaseRenderer):
|
||||||
|
media_type = "multipart/form-data; boundary=BoUnDaRyStRiNg"
|
||||||
|
format = "multipart"
|
||||||
|
charset = "utf-8"
|
||||||
|
BOUNDARY = "BoUnDaRyStRiNg"
|
||||||
|
|
||||||
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
|
return encode_multipart(self.BOUNDARY, data)
|
||||||
|
|
|
@ -0,0 +1,440 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
The Request class is used as a wrapper around the standard request object.
|
||||||
|
|
||||||
|
The wrapped request then offers a richer API, in particular :
|
||||||
|
|
||||||
|
- content automatically parsed according to `Content-Type` header,
|
||||||
|
and available as `request.DATA`
|
||||||
|
- full support of PUT method, including support for file uploads
|
||||||
|
- form overloading of HTTP method, content type and content
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import QueryDict
|
||||||
|
from django.http.multipartparser import parse_header
|
||||||
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
from django.utils.six import BytesIO
|
||||||
|
|
||||||
|
from taiga.base import exceptions
|
||||||
|
|
||||||
|
from . import HTTP_HEADER_ENCODING
|
||||||
|
from .settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
def is_form_media_type(media_type):
|
||||||
|
"""
|
||||||
|
Return True if the media type is a valid form media type.
|
||||||
|
"""
|
||||||
|
base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING))
|
||||||
|
return (base_media_type == "application/x-www-form-urlencoded" or
|
||||||
|
base_media_type == "multipart/form-data")
|
||||||
|
|
||||||
|
|
||||||
|
class override_method(object):
|
||||||
|
"""
|
||||||
|
A context manager that temporarily overrides the method on a request,
|
||||||
|
additionally setting the `view.request` attribute.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
with override_method(view, request, "POST") as request:
|
||||||
|
... # Do stuff with `view` and `request`
|
||||||
|
"""
|
||||||
|
def __init__(self, view, request, method):
|
||||||
|
self.view = view
|
||||||
|
self.request = request
|
||||||
|
self.method = method
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.view.request = clone_request(self.request, self.method)
|
||||||
|
return self.view.request
|
||||||
|
|
||||||
|
def __exit__(self, *args, **kwarg):
|
||||||
|
self.view.request = self.request
|
||||||
|
|
||||||
|
|
||||||
|
class Empty(object):
|
||||||
|
"""
|
||||||
|
Placeholder for unset attributes.
|
||||||
|
Cannot use `None`, as that may be a valid value.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _hasattr(obj, name):
|
||||||
|
return not getattr(obj, name) is Empty
|
||||||
|
|
||||||
|
|
||||||
|
def clone_request(request, method):
|
||||||
|
"""
|
||||||
|
Internal helper method to clone a request, replacing with a different
|
||||||
|
HTTP method. Used for checking permissions against other methods.
|
||||||
|
"""
|
||||||
|
ret = Request(request=request._request,
|
||||||
|
parsers=request.parsers,
|
||||||
|
authenticators=request.authenticators,
|
||||||
|
negotiator=request.negotiator,
|
||||||
|
parser_context=request.parser_context)
|
||||||
|
ret._data = request._data
|
||||||
|
ret._files = request._files
|
||||||
|
ret._content_type = request._content_type
|
||||||
|
ret._stream = request._stream
|
||||||
|
ret._method = method
|
||||||
|
if hasattr(request, "_user"):
|
||||||
|
ret._user = request._user
|
||||||
|
if hasattr(request, "_auth"):
|
||||||
|
ret._auth = request._auth
|
||||||
|
if hasattr(request, "_authenticator"):
|
||||||
|
ret._authenticator = request._authenticator
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class ForcedAuthentication(object):
|
||||||
|
"""
|
||||||
|
This authentication class is used if the test client or request factory
|
||||||
|
forcibly authenticated the request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, force_user, force_token):
|
||||||
|
self.force_user = force_user
|
||||||
|
self.force_token = force_token
|
||||||
|
|
||||||
|
def authenticate(self, request):
|
||||||
|
return (self.force_user, self.force_token)
|
||||||
|
|
||||||
|
|
||||||
|
class Request(object):
|
||||||
|
"""
|
||||||
|
Wrapper allowing to enhance a standard `HttpRequest` instance.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
- request(HttpRequest). The original request instance.
|
||||||
|
- parsers_classes(list/tuple). The parsers to use for parsing the
|
||||||
|
request content.
|
||||||
|
- authentication_classes(list/tuple). The authentications used to try
|
||||||
|
authenticating the request's user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE
|
||||||
|
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
|
||||||
|
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||||
|
|
||||||
|
def __init__(self, request, parsers=None, authenticators=None,
|
||||||
|
negotiator=None, parser_context=None):
|
||||||
|
self._request = request
|
||||||
|
self.parsers = parsers or ()
|
||||||
|
self.authenticators = authenticators or ()
|
||||||
|
self.negotiator = negotiator or self._default_negotiator()
|
||||||
|
self.parser_context = parser_context
|
||||||
|
self._data = Empty
|
||||||
|
self._files = Empty
|
||||||
|
self._method = Empty
|
||||||
|
self._content_type = Empty
|
||||||
|
self._stream = Empty
|
||||||
|
|
||||||
|
if self.parser_context is None:
|
||||||
|
self.parser_context = {}
|
||||||
|
self.parser_context["request"] = self
|
||||||
|
self.parser_context["encoding"] = request.encoding or settings.DEFAULT_CHARSET
|
||||||
|
|
||||||
|
force_user = getattr(request, "_force_auth_user", None)
|
||||||
|
force_token = getattr(request, "_force_auth_token", None)
|
||||||
|
if (force_user is not None or force_token is not None):
|
||||||
|
forced_auth = ForcedAuthentication(force_user, force_token)
|
||||||
|
self.authenticators = (forced_auth,)
|
||||||
|
|
||||||
|
def _default_negotiator(self):
|
||||||
|
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def method(self):
|
||||||
|
"""
|
||||||
|
Returns the HTTP method.
|
||||||
|
|
||||||
|
This allows the `method` to be overridden by using a hidden `form`
|
||||||
|
field on a form POST request.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, "_method"):
|
||||||
|
self._load_method_and_content_type()
|
||||||
|
return self._method
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_type(self):
|
||||||
|
"""
|
||||||
|
Returns the content type header.
|
||||||
|
|
||||||
|
This should be used instead of `request.META.get("HTTP_CONTENT_TYPE")`,
|
||||||
|
as it allows the content type to be overridden by using a hidden form
|
||||||
|
field on a form POST request.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, "_content_type"):
|
||||||
|
self._load_method_and_content_type()
|
||||||
|
return self._content_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stream(self):
|
||||||
|
"""
|
||||||
|
Returns an object that may be used to stream the request content.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, "_stream"):
|
||||||
|
self._load_stream()
|
||||||
|
return self._stream
|
||||||
|
|
||||||
|
@property
|
||||||
|
def QUERY_PARAMS(self):
|
||||||
|
"""
|
||||||
|
More semantically correct name for request.GET.
|
||||||
|
"""
|
||||||
|
return self._request.GET
|
||||||
|
|
||||||
|
@property
|
||||||
|
def DATA(self):
|
||||||
|
"""
|
||||||
|
Parses the request body and returns the data.
|
||||||
|
|
||||||
|
Similar to usual behaviour of `request.POST`, except that it handles
|
||||||
|
arbitrary parsers, and also works on methods other than POST (eg PUT).
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, "_data"):
|
||||||
|
self._load_data_and_files()
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def FILES(self):
|
||||||
|
"""
|
||||||
|
Parses the request body and returns any files uploaded in the request.
|
||||||
|
|
||||||
|
Similar to usual behaviour of `request.FILES`, except that it handles
|
||||||
|
arbitrary parsers, and also works on methods other than POST (eg PUT).
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, "_files"):
|
||||||
|
self._load_data_and_files()
|
||||||
|
return self._files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self):
|
||||||
|
"""
|
||||||
|
Returns the user associated with the current request, as authenticated
|
||||||
|
by the authentication classes provided to the request.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "_user"):
|
||||||
|
self._authenticate()
|
||||||
|
return self._user
|
||||||
|
|
||||||
|
@user.setter
|
||||||
|
def user(self, value):
|
||||||
|
"""
|
||||||
|
Sets the user on the current request. This is necessary to maintain
|
||||||
|
compatibility with django.contrib.auth where the user property is
|
||||||
|
set in the login and logout functions.
|
||||||
|
"""
|
||||||
|
self._user = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth(self):
|
||||||
|
"""
|
||||||
|
Returns any non-user authentication information associated with the
|
||||||
|
request, such as an authentication token.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "_auth"):
|
||||||
|
self._authenticate()
|
||||||
|
return self._auth
|
||||||
|
|
||||||
|
@auth.setter
|
||||||
|
def auth(self, value):
|
||||||
|
"""
|
||||||
|
Sets any non-user authentication information associated with the
|
||||||
|
request, such as an authentication token.
|
||||||
|
"""
|
||||||
|
self._auth = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def successful_authenticator(self):
|
||||||
|
"""
|
||||||
|
Return the instance of the authentication instance class that was used
|
||||||
|
to authenticate the request, or `None`.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, "_authenticator"):
|
||||||
|
self._authenticate()
|
||||||
|
return self._authenticator
|
||||||
|
|
||||||
|
def _load_data_and_files(self):
|
||||||
|
"""
|
||||||
|
Parses the request content into self.DATA and self.FILES.
|
||||||
|
"""
|
||||||
|
if not _hasattr(self, "_content_type"):
|
||||||
|
self._load_method_and_content_type()
|
||||||
|
|
||||||
|
if not _hasattr(self, "_data"):
|
||||||
|
self._data, self._files = self._parse()
|
||||||
|
|
||||||
|
def _load_method_and_content_type(self):
|
||||||
|
"""
|
||||||
|
Sets the method and content_type, and then check if they"ve
|
||||||
|
been overridden.
|
||||||
|
"""
|
||||||
|
self._content_type = self.META.get("HTTP_CONTENT_TYPE",
|
||||||
|
self.META.get("CONTENT_TYPE", ""))
|
||||||
|
|
||||||
|
self._perform_form_overloading()
|
||||||
|
|
||||||
|
if not _hasattr(self, "_method"):
|
||||||
|
self._method = self._request.method
|
||||||
|
|
||||||
|
# Allow X-HTTP-METHOD-OVERRIDE header
|
||||||
|
self._method = self.META.get("HTTP_X_HTTP_METHOD_OVERRIDE",
|
||||||
|
self._method)
|
||||||
|
|
||||||
|
def _load_stream(self):
|
||||||
|
"""
|
||||||
|
Return the content body of the request, as a stream.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
content_length = int(self.META.get("CONTENT_LENGTH",
|
||||||
|
self.META.get("HTTP_CONTENT_LENGTH")))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
content_length = 0
|
||||||
|
|
||||||
|
if content_length == 0:
|
||||||
|
self._stream = None
|
||||||
|
elif hasattr(self._request, "read"):
|
||||||
|
self._stream = self._request
|
||||||
|
else:
|
||||||
|
self._stream = BytesIO(self.raw_post_data)
|
||||||
|
|
||||||
|
def _perform_form_overloading(self):
|
||||||
|
"""
|
||||||
|
If this is a form POST request, then we need to check if the method and
|
||||||
|
content/content_type have been overridden by setting them in hidden
|
||||||
|
form fields or not.
|
||||||
|
"""
|
||||||
|
|
||||||
|
USE_FORM_OVERLOADING = (
|
||||||
|
self._METHOD_PARAM or
|
||||||
|
(self._CONTENT_PARAM and self._CONTENTTYPE_PARAM)
|
||||||
|
)
|
||||||
|
|
||||||
|
# We only need to use form overloading on form POST requests.
|
||||||
|
if (not USE_FORM_OVERLOADING
|
||||||
|
or self._request.method != "POST"
|
||||||
|
or not is_form_media_type(self._content_type)):
|
||||||
|
return
|
||||||
|
|
||||||
|
# At this point we"re committed to parsing the request as form data.
|
||||||
|
self._data = self._request.POST
|
||||||
|
self._files = self._request.FILES
|
||||||
|
|
||||||
|
# Method overloading - change the method and remove the param from the content.
|
||||||
|
if (self._METHOD_PARAM and
|
||||||
|
self._METHOD_PARAM in self._data):
|
||||||
|
self._method = self._data[self._METHOD_PARAM].upper()
|
||||||
|
|
||||||
|
# Content overloading - modify the content type, and force re-parse.
|
||||||
|
if (self._CONTENT_PARAM and
|
||||||
|
self._CONTENTTYPE_PARAM and
|
||||||
|
self._CONTENT_PARAM in self._data and
|
||||||
|
self._CONTENTTYPE_PARAM in self._data):
|
||||||
|
self._content_type = self._data[self._CONTENTTYPE_PARAM]
|
||||||
|
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context["encoding"]))
|
||||||
|
self._data, self._files = (Empty, Empty)
|
||||||
|
|
||||||
|
def _parse(self):
|
||||||
|
"""
|
||||||
|
Parse the request content, returning a two-tuple of (data, files)
|
||||||
|
|
||||||
|
May raise an `UnsupportedMediaType`, or `ParseError` exception.
|
||||||
|
"""
|
||||||
|
stream = self.stream
|
||||||
|
media_type = self.content_type
|
||||||
|
|
||||||
|
if stream is None or media_type is None:
|
||||||
|
empty_data = QueryDict("", self._request._encoding)
|
||||||
|
empty_files = MultiValueDict()
|
||||||
|
return (empty_data, empty_files)
|
||||||
|
|
||||||
|
parser = self.negotiator.select_parser(self, self.parsers)
|
||||||
|
|
||||||
|
if not parser:
|
||||||
|
raise exceptions.UnsupportedMediaType(media_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||||
|
except:
|
||||||
|
# If we get an exception during parsing, fill in empty data and
|
||||||
|
# re-raise. Ensures we don't simply repeat the error when
|
||||||
|
# attempting to render the browsable renderer response, or when
|
||||||
|
# logging the request or similar.
|
||||||
|
self._data = QueryDict("", self._request._encoding)
|
||||||
|
self._files = MultiValueDict()
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Parser classes may return the raw data, or a
|
||||||
|
# DataAndFiles object. Unpack the result as required.
|
||||||
|
try:
|
||||||
|
return (parsed.data, parsed.files)
|
||||||
|
except AttributeError:
|
||||||
|
empty_files = MultiValueDict()
|
||||||
|
return (parsed, empty_files)
|
||||||
|
|
||||||
|
def _authenticate(self):
|
||||||
|
"""
|
||||||
|
Attempt to authenticate the request using each authentication instance
|
||||||
|
in turn.
|
||||||
|
Returns a three-tuple of (authenticator, user, authtoken).
|
||||||
|
"""
|
||||||
|
for authenticator in self.authenticators:
|
||||||
|
try:
|
||||||
|
user_auth_tuple = authenticator.authenticate(self)
|
||||||
|
except exceptions.APIException:
|
||||||
|
self._not_authenticated()
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not user_auth_tuple is None:
|
||||||
|
self._authenticator = authenticator
|
||||||
|
self._user, self._auth = user_auth_tuple
|
||||||
|
return
|
||||||
|
|
||||||
|
self._not_authenticated()
|
||||||
|
|
||||||
|
def _not_authenticated(self):
|
||||||
|
"""
|
||||||
|
Return a three-tuple of (authenticator, user, authtoken), representing
|
||||||
|
an unauthenticated request.
|
||||||
|
|
||||||
|
By default this will be (None, AnonymousUser, None).
|
||||||
|
"""
|
||||||
|
self._authenticator = None
|
||||||
|
|
||||||
|
if api_settings.UNAUTHENTICATED_USER:
|
||||||
|
self._user = api_settings.UNAUTHENTICATED_USER()
|
||||||
|
else:
|
||||||
|
self._user = None
|
||||||
|
|
||||||
|
if api_settings.UNAUTHENTICATED_TOKEN:
|
||||||
|
self._auth = api_settings.UNAUTHENTICATED_TOKEN()
|
||||||
|
else:
|
||||||
|
self._auth = None
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
"""
|
||||||
|
Proxy other attributes to the underlying HttpRequest object.
|
||||||
|
"""
|
||||||
|
return getattr(self._request, attr)
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Provide reverse functions that return fully qualified URLs
|
||||||
|
"""
|
||||||
|
from django.core.urlresolvers import reverse as django_reverse
|
||||||
|
from django.utils.functional import lazy
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||||
|
"""
|
||||||
|
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
|
||||||
|
and returns a fully qualified URL, using the request to get the base URL.
|
||||||
|
"""
|
||||||
|
if format is not None:
|
||||||
|
kwargs = kwargs or {}
|
||||||
|
kwargs["format"] = format
|
||||||
|
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
|
||||||
|
if request:
|
||||||
|
return request.build_absolute_uri(url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
reverse_lazy = lazy(reverse, str)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,226 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2015, Tom Christie
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Settings for REST framework are all namespaced in the REST_FRAMEWORK setting.
|
||||||
|
For example your project's `settings.py` file might look like this:
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
"DEFAULT_RENDERER_CLASSES": (
|
||||||
|
"taiga.base.api.renderers.JSONRenderer",
|
||||||
|
)
|
||||||
|
"DEFAULT_PARSER_CLASSES": (
|
||||||
|
"taiga.base.api.parsers.JSONParser",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
This module provides the `api_setting` object, that is used to access
|
||||||
|
REST framework settings, checking for user settings first, then falling
|
||||||
|
back to the defaults.
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import importlib
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from . import ISO_8601
|
||||||
|
|
||||||
|
|
||||||
|
USER_SETTINGS = getattr(settings, "REST_FRAMEWORK", None)
|
||||||
|
|
||||||
|
DEFAULTS = {
|
||||||
|
# Base API policies
|
||||||
|
"DEFAULT_RENDERER_CLASSES": (
|
||||||
|
"taiga.base.api.renderers.JSONRenderer",
|
||||||
|
"taiga.base.api.renderers.BrowsableAPIRenderer",
|
||||||
|
),
|
||||||
|
"DEFAULT_PARSER_CLASSES": (
|
||||||
|
"taiga.base.api.parsers.JSONParser",
|
||||||
|
"taiga.base.api.parsers.FormParser",
|
||||||
|
"taiga.base.api.parsers.MultiPartParser"
|
||||||
|
),
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
|
"taiga.base.api.authentication.SessionAuthentication",
|
||||||
|
"taiga.base.api.authentication.BasicAuthentication"
|
||||||
|
),
|
||||||
|
"DEFAULT_PERMISSION_CLASSES": (
|
||||||
|
"taiga.base.api.permissions.AllowAny",
|
||||||
|
),
|
||||||
|
"DEFAULT_THROTTLE_CLASSES": (
|
||||||
|
),
|
||||||
|
"DEFAULT_CONTENT_NEGOTIATION_CLASS":
|
||||||
|
"taiga.base.api.negotiation.DefaultContentNegotiation",
|
||||||
|
|
||||||
|
# Genric view behavior
|
||||||
|
"DEFAULT_MODEL_SERIALIZER_CLASS":
|
||||||
|
"taiga.base.api.serializers.ModelSerializer",
|
||||||
|
"DEFAULT_FILTER_BACKENDS": (),
|
||||||
|
|
||||||
|
# Throttling
|
||||||
|
"DEFAULT_THROTTLE_RATES": {
|
||||||
|
"user": None,
|
||||||
|
"anon": None,
|
||||||
|
},
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
"PAGINATE_BY": None,
|
||||||
|
"PAGINATE_BY_PARAM": None,
|
||||||
|
"MAX_PAGINATE_BY": None,
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
"UNAUTHENTICATED_USER": "django.contrib.auth.models.AnonymousUser",
|
||||||
|
"UNAUTHENTICATED_TOKEN": None,
|
||||||
|
|
||||||
|
# View configuration
|
||||||
|
"VIEW_NAME_FUNCTION": "taiga.base.api.views.get_view_name",
|
||||||
|
"VIEW_DESCRIPTION_FUNCTION": "taiga.base.api.views.get_view_description",
|
||||||
|
|
||||||
|
# Exception handling
|
||||||
|
"EXCEPTION_HANDLER": "taiga.base.api.views.exception_handler",
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
"TEST_REQUEST_RENDERER_CLASSES": (
|
||||||
|
"taiga.base.api.renderers.MultiPartRenderer",
|
||||||
|
"taiga.base.api.renderers.JSONRenderer"
|
||||||
|
),
|
||||||
|
"TEST_REQUEST_DEFAULT_FORMAT": "multipart",
|
||||||
|
|
||||||
|
# Browser enhancements
|
||||||
|
"FORM_METHOD_OVERRIDE": "_method",
|
||||||
|
"FORM_CONTENT_OVERRIDE": "_content",
|
||||||
|
"FORM_CONTENTTYPE_OVERRIDE": "_content_type",
|
||||||
|
"URL_ACCEPT_OVERRIDE": "accept",
|
||||||
|
"URL_FORMAT_OVERRIDE": "format",
|
||||||
|
|
||||||
|
"FORMAT_SUFFIX_KWARG": "format",
|
||||||
|
"URL_FIELD_NAME": "url",
|
||||||
|
|
||||||
|
# Input and output formats
|
||||||
|
"DATE_INPUT_FORMATS": (
|
||||||
|
ISO_8601,
|
||||||
|
),
|
||||||
|
"DATE_FORMAT": None,
|
||||||
|
|
||||||
|
"DATETIME_INPUT_FORMATS": (
|
||||||
|
ISO_8601,
|
||||||
|
),
|
||||||
|
"DATETIME_FORMAT": None,
|
||||||
|
|
||||||
|
"TIME_INPUT_FORMATS": (
|
||||||
|
ISO_8601,
|
||||||
|
),
|
||||||
|
"TIME_FORMAT": None,
|
||||||
|
|
||||||
|
# Pending deprecation
|
||||||
|
"FILTER_BACKEND": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# List of settings that may be in string import notation.
|
||||||
|
IMPORT_STRINGS = (
|
||||||
|
"DEFAULT_RENDERER_CLASSES",
|
||||||
|
"DEFAULT_PARSER_CLASSES",
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES",
|
||||||
|
"DEFAULT_PERMISSION_CLASSES",
|
||||||
|
"DEFAULT_THROTTLE_CLASSES",
|
||||||
|
"DEFAULT_CONTENT_NEGOTIATION_CLASS",
|
||||||
|
"DEFAULT_MODEL_SERIALIZER_CLASS",
|
||||||
|
"DEFAULT_FILTER_BACKENDS",
|
||||||
|
"EXCEPTION_HANDLER",
|
||||||
|
"FILTER_BACKEND",
|
||||||
|
"TEST_REQUEST_RENDERER_CLASSES",
|
||||||
|
"UNAUTHENTICATED_USER",
|
||||||
|
"UNAUTHENTICATED_TOKEN",
|
||||||
|
"VIEW_NAME_FUNCTION",
|
||||||
|
"VIEW_DESCRIPTION_FUNCTION"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_import(val, setting_name):
|
||||||
|
"""
|
||||||
|
If the given setting is a string import notation,
|
||||||
|
then perform the necessary import or imports.
|
||||||
|
"""
|
||||||
|
if isinstance(val, six.string_types):
|
||||||
|
return import_from_string(val, setting_name)
|
||||||
|
elif isinstance(val, (list, tuple)):
|
||||||
|
return [import_from_string(item, setting_name) for item in val]
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def import_from_string(val, setting_name):
|
||||||
|
"""
|
||||||
|
Attempt to import a class from a string representation.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Nod to tastypie's use of importlib.
|
||||||
|
parts = val.split('.')
|
||||||
|
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
return getattr(module, class_name)
|
||||||
|
except ImportError as e:
|
||||||
|
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||||
|
raise ImportError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class APISettings(object):
|
||||||
|
"""
|
||||||
|
A settings object, that allows API settings to be accessed as properties.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
from taiga.base.api.settings import api_settings
|
||||||
|
print api_settings.DEFAULT_RENDERER_CLASSES
|
||||||
|
|
||||||
|
Any setting with string import paths will be automatically resolved
|
||||||
|
and return the class, rather than the string literal.
|
||||||
|
"""
|
||||||
|
def __init__(self, user_settings=None, defaults=None, import_strings=None):
|
||||||
|
self.user_settings = user_settings or {}
|
||||||
|
self.defaults = defaults or {}
|
||||||
|
self.import_strings = import_strings or ()
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
if attr not in self.defaults.keys():
|
||||||
|
raise AttributeError("Invalid API setting: '%s'" % attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if present in user settings
|
||||||
|
val = self.user_settings[attr]
|
||||||
|
except KeyError:
|
||||||
|
# Fall back to defaults
|
||||||
|
val = self.defaults[attr]
|
||||||
|
|
||||||
|
# Coerce import strings into classes
|
||||||
|
if val and attr in self.import_strings:
|
||||||
|
val = perform_import(val, attr)
|
||||||
|
|
||||||
|
self.validate_setting(attr, val)
|
||||||
|
|
||||||
|
# Cache the result
|
||||||
|
setattr(self, attr, val)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def validate_setting(self, attr, val):
|
||||||
|
if attr == "FILTER_BACKEND" and val is not None:
|
||||||
|
# Make sure we can initialize the class
|
||||||
|
val()
|
||||||
|
|
||||||
|
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
|
@ -0,0 +1,206 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is partially taken from django-rest-framework:
|
||||||
|
* Copyright (c) 2011-2014, Tom Christie
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This CSS file contains some tweaks specific to the included Bootstrap theme.
|
||||||
|
It's separate from `style.css` so that it can be easily overridden by replacing
|
||||||
|
a single block in the template.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
background: transparent;
|
||||||
|
border-top-color: transparent;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-inverse .brand a {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.navbar-inverse .brand:hover a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom navigation styles */
|
||||||
|
.wrapper .navbar{
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner{
|
||||||
|
background: #2C2C2C;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-top: 5px solid #A30000;
|
||||||
|
border-radius: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list > .active > a, .nav-list > .active > a:hover {
|
||||||
|
background: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
.navbar .navbar-inner .dropdown-menu li a:hover{
|
||||||
|
background: #eeeeee;
|
||||||
|
color: #c20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=== dabapps bootstrap styles ====*/
|
||||||
|
|
||||||
|
html{
|
||||||
|
width:100%;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, .navbar .navbar-inner .container-fluid {
|
||||||
|
max-width: 1150px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
background: url("../img/grid.png") repeat-x;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sticky footer and footer */
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-switcher {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well {
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well .form-actions {
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.well form .help-block {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > li {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs li a {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > .active > a {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tabs > .active > a:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabbable.first-tab-active .tab-content
|
||||||
|
{
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer, #push {
|
||||||
|
height: 60px; /* .push must be the same height as .footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer p {
|
||||||
|
text-align: center;
|
||||||
|
color: gray;
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a {
|
||||||
|
color: gray;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a:hover {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom general page styles */
|
||||||
|
.hero-unit h2, .hero-unit h1{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a, body a{
|
||||||
|
color: #A30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
body a:hover{
|
||||||
|
color: #c20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content a span{
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-info {
|
||||||
|
clear:both;
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is partially taken from django-rest-framework:
|
||||||
|
* Copyright (c) 2011-2014, Tom Christie
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* The navbar is fixed at >= 980px wide, so add padding to the body to prevent
|
||||||
|
content running up underneath it. */
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3 {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-description, .response-info {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
.version:before {
|
||||||
|
content: "v";
|
||||||
|
opacity: 0.6;
|
||||||
|
padding-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-option {
|
||||||
|
font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-form {
|
||||||
|
float: right;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.breadcrumb {
|
||||||
|
margin: 58px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form select, form input, form textarea {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form select[multiple] {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
/* To allow tooltips to work on disabled elements */
|
||||||
|
.disabled-tooltip-shield {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorlist {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
word-wrap: normal;
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is partially taken from django-rest-framework:
|
||||||
|
* Copyright (c) 2011-2014, Tom Christie
|
||||||
|
*/
|
||||||
|
.com { color: #93a1a1; }
|
||||||
|
.lit { color: #195f91; }
|
||||||
|
.pun, .opn, .clo { color: #93a1a1; }
|
||||||
|
.fun { color: #dc322f; }
|
||||||
|
.str, .atv { color: #D14; }
|
||||||
|
.kwd, .prettyprint .tag { color: #1e347b; }
|
||||||
|
.typ, .atn, .dec, .var { color: teal; }
|
||||||
|
.pln { color: #48484c; }
|
||||||
|
|
||||||
|
.prettyprint {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f7f7f9;
|
||||||
|
border: 1px solid #e1e1e8;
|
||||||
|
}
|
||||||
|
.prettyprint.linenums {
|
||||||
|
-webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
|
||||||
|
-moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
|
||||||
|
box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specify class=linenums on a pre to get line numbering */
|
||||||
|
ol.linenums {
|
||||||
|
margin: 0 0 0 33px; /* IE indents via margin-left */
|
||||||
|
}
|
||||||
|
ol.linenums li {
|
||||||
|
padding-left: 12px;
|
||||||
|
color: #bebec5;
|
||||||
|
line-height: 20px;
|
||||||
|
text-shadow: 0 1px 0 #fff;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is partially taken from django-rest-framework:
|
||||||
|
* Copyright (c) 2011-2014, Tom Christie
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function getCookie(c_name)
|
||||||
|
{
|
||||||
|
// From http://www.w3schools.com/js/js_cookies.asp
|
||||||
|
var c_value = document.cookie;
|
||||||
|
var c_start = c_value.indexOf(" " + c_name + "=");
|
||||||
|
if (c_start == -1) {
|
||||||
|
c_start = c_value.indexOf(c_name + "=");
|
||||||
|
}
|
||||||
|
if (c_start == -1) {
|
||||||
|
c_value = null;
|
||||||
|
} else {
|
||||||
|
c_start = c_value.indexOf("=", c_start) + 1;
|
||||||
|
var c_end = c_value.indexOf(";", c_start);
|
||||||
|
if (c_end == -1) {
|
||||||
|
c_end = c_value.length;
|
||||||
|
}
|
||||||
|
c_value = unescape(c_value.substring(c_start,c_end));
|
||||||
|
}
|
||||||
|
return c_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON highlighting.
|
||||||
|
prettyPrint();
|
||||||
|
|
||||||
|
// Bootstrap tooltips.
|
||||||
|
$('.js-tooltip').tooltip({
|
||||||
|
delay: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deal with rounded tab styling after tab clicks.
|
||||||
|
$('a[data-toggle="tab"]:first').on('shown', function (e) {
|
||||||
|
$(e.target).parents('.tabbable').addClass('first-tab-active');
|
||||||
|
});
|
||||||
|
$('a[data-toggle="tab"]:not(:first)').on('shown', function (e) {
|
||||||
|
$(e.target).parents('.tabbable').removeClass('first-tab-active');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('a[data-toggle="tab"]').click(function(){
|
||||||
|
document.cookie="tabstyle=" + this.name + "; path=/";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store tab preference in cookies & display appropriate tab on load.
|
||||||
|
var selectedTab = null;
|
||||||
|
var selectedTabName = getCookie('tabstyle');
|
||||||
|
|
||||||
|
if (selectedTabName) {
|
||||||
|
selectedTab = $('.form-switcher a[name=' + selectedTabName + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTab && selectedTab.length > 0) {
|
||||||
|
// Display whichever tab is selected.
|
||||||
|
selectedTab.tab('show');
|
||||||
|
} else {
|
||||||
|
// If no tab selected, display rightmost tab.
|
||||||
|
$('.form-switcher a:first').tab('show');
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* This code is partially taken from django-rest-framework:
|
||||||
|
* Copyright (c) 2011-2014, Tom Christie
|
||||||
|
*/
|
||||||
|
var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
|
||||||
|
(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
|
||||||
|
[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
|
||||||
|
f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
|
||||||
|
(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
|
||||||
|
{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
|
||||||
|
t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
|
||||||
|
"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
|
||||||
|
l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
|
||||||
|
q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
|
||||||
|
q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
|
||||||
|
"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
|
||||||
|
a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
|
||||||
|
for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
|
||||||
|
m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
|
||||||
|
a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
|
||||||
|
j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
|
||||||
|
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
|
||||||
|
H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
|
||||||
|
J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
|
||||||
|
I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
|
||||||
|
["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
|
||||||
|
/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
|
||||||
|
["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
|
||||||
|
hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
|
||||||
|
!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
|
||||||
|
250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
|
||||||
|
PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "api/base.html" %}
|
||||||
|
|
||||||
|
{# Override this template in your own templates directory to customize #}
|
|
@ -0,0 +1,237 @@
|
||||||
|
{% load url from future %}
|
||||||
|
{% load api %}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% block head %}
|
||||||
|
|
||||||
|
{% block meta %}
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<title>{% block title %}Taiga API REST{% endblock %}</title>
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
{% block bootstrap_theme %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap.min.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap-tweaks.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/prettify.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/default.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="{% block bodyclass %}{% endblock %} container">
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
|
||||||
|
{% block navbar %}
|
||||||
|
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<span href="/">
|
||||||
|
{% block branding %}
|
||||||
|
<a class='brand' rel="nofollow" href='https://taiga.io'>
|
||||||
|
Taiga API REST
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
|
</span>
|
||||||
|
<ul class="nav pull-right">
|
||||||
|
{% block userlinks %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
{{ user }}
|
||||||
|
<b class="caret"></b>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>{% optional_logout request %}</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>{% optional_login request %}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<ul class="breadcrumb">
|
||||||
|
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">›</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
{% if 'GET' in allowed_methods %}
|
||||||
|
<form id="get-form" class="pull-right">
|
||||||
|
<fieldset>
|
||||||
|
<div class="btn-group format-selection">
|
||||||
|
<a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
|
||||||
|
|
||||||
|
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
{% for format in available_formats %}
|
||||||
|
<li>
|
||||||
|
<a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if options_form %}
|
||||||
|
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
|
||||||
|
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if delete_form %}
|
||||||
|
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
||||||
|
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="content-main">
|
||||||
|
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||||
|
{% block description %}
|
||||||
|
{{ description }}
|
||||||
|
{% endblock %}
|
||||||
|
<div class="request-info" style="clear: both" >
|
||||||
|
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="response-info">
|
||||||
|
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||||
|
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if display_edit_forms %}
|
||||||
|
|
||||||
|
{% if post_form or raw_data_post_form %}
|
||||||
|
<div {% if post_form %}class="tabbable"{% endif %}>
|
||||||
|
{% if post_form %}
|
||||||
|
<ul class="nav nav-tabs form-switcher">
|
||||||
|
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||||
|
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<div class="well tab-content">
|
||||||
|
{% if post_form %}
|
||||||
|
<div class="tab-pane" id="object-form">
|
||||||
|
{% with form=post_form %}
|
||||||
|
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
{{ post_form }}
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form">
|
||||||
|
{% with form=raw_data_post_form %}
|
||||||
|
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
{% include "api/raw_data_form.html" %}
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if put_form or raw_data_put_form or raw_data_patch_form %}
|
||||||
|
<div {% if put_form %}class="tabbable"{% endif %}>
|
||||||
|
{% if put_form %}
|
||||||
|
<ul class="nav nav-tabs form-switcher">
|
||||||
|
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||||
|
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
<div class="well tab-content">
|
||||||
|
{% if put_form %}
|
||||||
|
<div class="tab-pane" id="object-form">
|
||||||
|
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
{{ put_form }}
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
|
||||||
|
{% with form=raw_data_put_or_patch_form %}
|
||||||
|
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
{% include "api/raw_data_form.html" %}
|
||||||
|
<div class="form-actions">
|
||||||
|
{% if raw_data_put_form %}
|
||||||
|
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if raw_data_patch_form %}
|
||||||
|
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END content-main -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- END Content -->
|
||||||
|
|
||||||
|
<div id="push"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- ./wrapper -->
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script src="{% static "api/js/jquery-1.8.1-min.js" %}"></script>
|
||||||
|
<script src="{% static "api/js/bootstrap.min.js" %}"></script>
|
||||||
|
<script src="{% static "api/js/prettify-min.js" %}"></script>
|
||||||
|
<script src="{% static "api/js/default.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% load api %}
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% for field in form.fields.values %}
|
||||||
|
{% if not field.read_only %}
|
||||||
|
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||||
|
{{ field.label_tag|add_class:"control-label" }}
|
||||||
|
<div class="controls">
|
||||||
|
{{ field.widget_html }}
|
||||||
|
{% if field.help_text %}<span class="help-block">{{ field.help_text }}</span>{% endif %}
|
||||||
|
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "api/login_base.html" %}
|
||||||
|
|
||||||
|
{# Override this template in your own templates directory to customize #}
|
|
@ -0,0 +1,53 @@
|
||||||
|
{% load url from future %}
|
||||||
|
{% load api %}
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
{% block style %}
|
||||||
|
{% block bootstrap_theme %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap.min.css" %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap-tweaks.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "api/css/default.css" %}"/>
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="container">
|
||||||
|
|
||||||
|
<div class="container-fluid" style="margin-top: 30px">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div>
|
||||||
|
{% block branding %}<h3 style="margin: 0 0 20px;">Taiga API REST</h3>{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div><!-- /row fluid -->
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div>
|
||||||
|
<form action="{% url 'api:login' %}" class=" form-inline" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div id="div_id_username" class="clearfix control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<Label class="span4">Username:</label>
|
||||||
|
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="div_id_password" class="clearfix control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<Label class="span4">Password:</label>
|
||||||
|
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
<div class="form-actions-no-box">
|
||||||
|
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.row-fluid -->
|
||||||
|
</div><!--/.well-->
|
||||||
|
</div><!-- /.row-fluid -->
|
||||||
|
</div><!-- /.container-fluid -->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% load api %}
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.non_field_errors }}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="control-group">
|
||||||
|
{{ field.label_tag|add_class:"control-label" }}
|
||||||
|
<div class="controls">
|
||||||
|
{{ field }}
|
||||||
|
<span class="help-block">{{ field.help_text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,233 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
|
from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
|
||||||
|
from django.http import QueryDict
|
||||||
|
from django.utils.encoding import iri_to_uri
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import SafeData, mark_safe
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.html import smart_urlquote
|
||||||
|
|
||||||
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag("static")
|
||||||
|
def do_static(parser, token):
|
||||||
|
return StaticFilesNode.handle_token(parser, token)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_query_param(url, key, val):
|
||||||
|
"""
|
||||||
|
Given a URL and a key/val pair, set or replace an item in the query
|
||||||
|
parameters of the URL, and return the new URL.
|
||||||
|
"""
|
||||||
|
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
|
||||||
|
query_dict = QueryDict(query).copy()
|
||||||
|
query_dict[key] = val
|
||||||
|
query = query_dict.urlencode()
|
||||||
|
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||||
|
|
||||||
|
|
||||||
|
# Regex for adding classes to html snippets
|
||||||
|
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
||||||
|
|
||||||
|
|
||||||
|
# And the template tags themselves...
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def optional_login(request):
|
||||||
|
"""
|
||||||
|
Include a login snippet if REST framework's login view is in the URLconf.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
login_url = reverse("api:login")
|
||||||
|
except NoReverseMatch:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
|
||||||
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def optional_logout(request):
|
||||||
|
"""
|
||||||
|
Include a logout snippet if REST framework's logout view is in the URLconf.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logout_url = reverse("api:logout")
|
||||||
|
except NoReverseMatch:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
|
||||||
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def add_query_param(request, key, val):
|
||||||
|
"""
|
||||||
|
Add a query parameter to the current request url, and return the new url.
|
||||||
|
"""
|
||||||
|
iri = request.get_full_path()
|
||||||
|
uri = iri_to_uri(iri)
|
||||||
|
return replace_query_param(uri, key, val)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def add_class(value, css_class):
|
||||||
|
"""
|
||||||
|
http://stackoverflow.com/questions/4124220/django-adding-css-classes-when-rendering-form-fields-in-a-template
|
||||||
|
|
||||||
|
Inserts classes into template variables that contain HTML tags,
|
||||||
|
useful for modifying forms without needing to change the Form objects.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
{{ field.label_tag|add_class:"control-label" }}
|
||||||
|
|
||||||
|
In the case of REST Framework, the filter is used to add Bootstrap-specific
|
||||||
|
classes to the forms.
|
||||||
|
"""
|
||||||
|
html = six.text_type(value)
|
||||||
|
match = class_re.search(html)
|
||||||
|
if match:
|
||||||
|
m = re.search(r"^%s$|^%s\s|\s%s\s|\s%s$" % (css_class, css_class,
|
||||||
|
css_class, css_class),
|
||||||
|
match.group(1))
|
||||||
|
if not m:
|
||||||
|
return mark_safe(class_re.sub(match.group(1) + " " + css_class,
|
||||||
|
html))
|
||||||
|
else:
|
||||||
|
return mark_safe(html.replace(">", ' class="%s">' % css_class, 1))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# Bunch of stuff cloned from urlize
|
||||||
|
TRAILING_PUNCTUATION = [".", ",", ":", ";", ".)", "\"", "'"]
|
||||||
|
WRAPPING_PUNCTUATION = [("(", ")"), ("<", ">"), ("[", "]"), ("<", ">"),
|
||||||
|
("\"", "\""), ("'", "'")]
|
||||||
|
word_split_re = re.compile(r"(\s+)")
|
||||||
|
simple_url_re = re.compile(r"^https?://\[?\w", re.IGNORECASE)
|
||||||
|
simple_url_2_re = re.compile(r"^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$", re.IGNORECASE)
|
||||||
|
simple_email_re = re.compile(r"^\S+@\S+\.\S+$")
|
||||||
|
|
||||||
|
|
||||||
|
def smart_urlquote_wrapper(matched_url):
|
||||||
|
"""
|
||||||
|
Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can
|
||||||
|
be raised here, see issue #1386
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return smart_urlquote(matched_url)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
||||||
|
"""
|
||||||
|
Converts any URLs in text into clickable links.
|
||||||
|
|
||||||
|
Works on http://, https://, www. links, and also on links ending in one of
|
||||||
|
the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org).
|
||||||
|
Links can have trailing punctuation (periods, commas, close-parens) and
|
||||||
|
leading punctuation (opening parens) and it"ll still do the right thing.
|
||||||
|
|
||||||
|
If trim_url_limit is not None, the URLs in link text longer than this limit
|
||||||
|
will truncated to trim_url_limit-3 characters and appended with an elipsis.
|
||||||
|
|
||||||
|
If nofollow is True, the URLs in link text will get a rel="nofollow"
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
If autoescape is True, the link text and URLs will get autoescaped.
|
||||||
|
"""
|
||||||
|
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ("%s..." % x[:max(0, limit - 3)])) or x
|
||||||
|
safe_input = isinstance(text, SafeData)
|
||||||
|
words = word_split_re.split(force_text(text))
|
||||||
|
for i, word in enumerate(words):
|
||||||
|
if "." in word or "@" in word or ":" in word:
|
||||||
|
# Deal with punctuation.
|
||||||
|
lead, middle, trail = "", word, ""
|
||||||
|
for punctuation in TRAILING_PUNCTUATION:
|
||||||
|
if middle.endswith(punctuation):
|
||||||
|
middle = middle[:-len(punctuation)]
|
||||||
|
trail = punctuation + trail
|
||||||
|
for opening, closing in WRAPPING_PUNCTUATION:
|
||||||
|
if middle.startswith(opening):
|
||||||
|
middle = middle[len(opening):]
|
||||||
|
lead = lead + opening
|
||||||
|
# Keep parentheses at the end only if they"re balanced.
|
||||||
|
if (middle.endswith(closing)
|
||||||
|
and middle.count(closing) == middle.count(opening) + 1):
|
||||||
|
middle = middle[:-len(closing)]
|
||||||
|
trail = closing + trail
|
||||||
|
|
||||||
|
# Make URL we want to point to.
|
||||||
|
url = None
|
||||||
|
nofollow_attr = ' rel="nofollow"' if nofollow else ""
|
||||||
|
if simple_url_re.match(middle):
|
||||||
|
url = smart_urlquote_wrapper(middle)
|
||||||
|
elif simple_url_2_re.match(middle):
|
||||||
|
url = smart_urlquote_wrapper("http://%s" % middle)
|
||||||
|
elif not ":" in middle and simple_email_re.match(middle):
|
||||||
|
local, domain = middle.rsplit("@", 1)
|
||||||
|
try:
|
||||||
|
domain = domain.encode("idna").decode("ascii")
|
||||||
|
except UnicodeError:
|
||||||
|
continue
|
||||||
|
url = "mailto:%s@%s" % (local, domain)
|
||||||
|
nofollow_attr = ""
|
||||||
|
|
||||||
|
# Make link.
|
||||||
|
if url:
|
||||||
|
trimmed = trim_url(middle)
|
||||||
|
if autoescape and not safe_input:
|
||||||
|
lead, trail = escape(lead), escape(trail)
|
||||||
|
url, trimmed = escape(url), escape(trimmed)
|
||||||
|
middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
|
||||||
|
words[i] = mark_safe("%s%s%s" % (lead, middle, trail))
|
||||||
|
else:
|
||||||
|
if safe_input:
|
||||||
|
words[i] = mark_safe(word)
|
||||||
|
elif autoescape:
|
||||||
|
words[i] = escape(word)
|
||||||
|
elif safe_input:
|
||||||
|
words[i] = mark_safe(word)
|
||||||
|
elif autoescape:
|
||||||
|
words[i] = escape(word)
|
||||||
|
return "".join(words)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def break_long_headers(header):
|
||||||
|
"""
|
||||||
|
Breaks headers longer than 160 characters (~page length)
|
||||||
|
when possible (are comma separated)
|
||||||
|
"""
|
||||||
|
if len(header) > 160 and "," in header:
|
||||||
|
header = mark_safe("<br> " + ", <br>".join(header.split(",")))
|
||||||
|
return header
|
|
@ -0,0 +1,255 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Provides various throttling policies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.core.cache import cache as default_cache
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from .settings import api_settings
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class BaseThrottle(object):
|
||||||
|
"""
|
||||||
|
Rate throttling of requests.
|
||||||
|
"""
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
"""
|
||||||
|
Return `True` if the request should be allowed, `False` otherwise.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(".allow_request() must be overridden")
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""
|
||||||
|
Optionally, return a recommended number of seconds to wait before
|
||||||
|
the next request.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleRateThrottle(BaseThrottle):
|
||||||
|
"""
|
||||||
|
A simple cache implementation, that only requires `.get_cache_key()`
|
||||||
|
to be overridden.
|
||||||
|
|
||||||
|
The rate (requests / seconds) is set by a `throttle` attribute on the View
|
||||||
|
class. The attribute is a string of the form "number_of_requests/period".
|
||||||
|
|
||||||
|
Period should be one of: ("s", "sec", "m", "min", "h", "hour", "d", "day")
|
||||||
|
|
||||||
|
Previous request information used for throttling is stored in the cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
cache = default_cache
|
||||||
|
timer = time.time
|
||||||
|
cache_format = "throtte_%(scope)s_%(ident)s"
|
||||||
|
scope = None
|
||||||
|
THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if not getattr(self, "rate", None):
|
||||||
|
self.rate = self.get_rate()
|
||||||
|
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
"""
|
||||||
|
Should return a unique cache-key which can be used for throttling.
|
||||||
|
Must be overridden.
|
||||||
|
|
||||||
|
May return `None` if the request should not be throttled.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(".get_cache_key() must be overridden")
|
||||||
|
|
||||||
|
def get_rate(self):
|
||||||
|
"""
|
||||||
|
Determine the string representation of the allowed request rate.
|
||||||
|
"""
|
||||||
|
if not getattr(self, "scope", None):
|
||||||
|
msg = ("You must set either `.scope` or `.rate` for \"%s\" throttle" %
|
||||||
|
self.__class__.__name__)
|
||||||
|
raise ImproperlyConfigured(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.THROTTLE_RATES[self.scope]
|
||||||
|
except KeyError:
|
||||||
|
msg = "No default throttle rate set for \"%s\" scope" % self.scope
|
||||||
|
raise ImproperlyConfigured(msg)
|
||||||
|
|
||||||
|
def parse_rate(self, rate):
|
||||||
|
"""
|
||||||
|
Given the request rate string, return a two tuple of:
|
||||||
|
<allowed number of requests>, <period of time in seconds>
|
||||||
|
"""
|
||||||
|
if rate is None:
|
||||||
|
return (None, None)
|
||||||
|
num, period = rate.split("/")
|
||||||
|
num_requests = int(num)
|
||||||
|
duration = {"s": 1, "m": 60, "h": 3600, "d": 86400}[period[0]]
|
||||||
|
return (num_requests, duration)
|
||||||
|
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
"""
|
||||||
|
Implement the check to see if the request should be throttled.
|
||||||
|
|
||||||
|
On success calls `throttle_success`.
|
||||||
|
On failure calls `throttle_failure`.
|
||||||
|
"""
|
||||||
|
if self.rate is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.key = self.get_cache_key(request, view)
|
||||||
|
if self.key is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.history = self.cache.get(self.key, [])
|
||||||
|
self.now = self.timer()
|
||||||
|
|
||||||
|
# Drop any requests from the history which have now passed the
|
||||||
|
# throttle duration
|
||||||
|
while self.history and self.history[-1] <= self.now - self.duration:
|
||||||
|
self.history.pop()
|
||||||
|
if len(self.history) >= self.num_requests:
|
||||||
|
return self.throttle_failure()
|
||||||
|
return self.throttle_success()
|
||||||
|
|
||||||
|
def throttle_success(self):
|
||||||
|
"""
|
||||||
|
Inserts the current request's timestamp along with the key
|
||||||
|
into the cache.
|
||||||
|
"""
|
||||||
|
self.history.insert(0, self.now)
|
||||||
|
self.cache.set(self.key, self.history, self.duration)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def throttle_failure(self):
|
||||||
|
"""
|
||||||
|
Called when a request to the API has failed due to throttling.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
"""
|
||||||
|
Returns the recommended next request time in seconds.
|
||||||
|
"""
|
||||||
|
if self.history:
|
||||||
|
remaining_duration = self.duration - (self.now - self.history[-1])
|
||||||
|
else:
|
||||||
|
remaining_duration = self.duration
|
||||||
|
|
||||||
|
available_requests = self.num_requests - len(self.history) + 1
|
||||||
|
if available_requests <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return remaining_duration / float(available_requests)
|
||||||
|
|
||||||
|
|
||||||
|
class AnonRateThrottle(SimpleRateThrottle):
|
||||||
|
"""
|
||||||
|
Limits the rate of API calls that may be made by a anonymous users.
|
||||||
|
|
||||||
|
The IP address of the request will be used as the unique cache key.
|
||||||
|
"""
|
||||||
|
scope = "anon"
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
return None # Only throttle unauthenticated requests.
|
||||||
|
|
||||||
|
ident = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||||
|
if ident is None:
|
||||||
|
ident = request.META.get("REMOTE_ADDR")
|
||||||
|
|
||||||
|
return self.cache_format % {
|
||||||
|
"scope": self.scope,
|
||||||
|
"ident": ident
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserRateThrottle(SimpleRateThrottle):
|
||||||
|
"""
|
||||||
|
Limits the rate of API calls that may be made by a given user.
|
||||||
|
|
||||||
|
The user id will be used as a unique cache key if the user is
|
||||||
|
authenticated. For anonymous requests, the IP address of the request will
|
||||||
|
be used.
|
||||||
|
"""
|
||||||
|
scope = "user"
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
ident = request.user.id
|
||||||
|
else:
|
||||||
|
ident = request.META.get("REMOTE_ADDR", None)
|
||||||
|
|
||||||
|
return self.cache_format % {
|
||||||
|
"scope": self.scope,
|
||||||
|
"ident": ident
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ScopedRateThrottle(SimpleRateThrottle):
|
||||||
|
"""
|
||||||
|
Limits the rate of API calls by different amounts for various parts of
|
||||||
|
the API. Any view that has the `throttle_scope` property set will be
|
||||||
|
throttled. The unique cache key will be generated by concatenating the
|
||||||
|
user id of the request, and the scope of the view being accessed.
|
||||||
|
"""
|
||||||
|
scope_attr = "throttle_scope"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Override the usual SimpleRateThrottle, because we can't determine
|
||||||
|
# the rate until called by the view.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
# We can only determine the scope once we"re called by the view.
|
||||||
|
self.scope = getattr(view, self.scope_attr, None)
|
||||||
|
|
||||||
|
# If a view does not have a `throttle_scope` always allow the request
|
||||||
|
if not self.scope:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Determine the allowed request rate as we normally would during
|
||||||
|
# the `__init__` call.
|
||||||
|
self.rate = self.get_rate()
|
||||||
|
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||||
|
|
||||||
|
# We can now proceed as normal.
|
||||||
|
return super(ScopedRateThrottle, self).allow_request(request, view)
|
||||||
|
|
||||||
|
def get_cache_key(self, request, view):
|
||||||
|
"""
|
||||||
|
If `view.throttle_scope` is not set, don't apply this throttle.
|
||||||
|
|
||||||
|
Otherwise generate the unique cache key by concatenating the user id
|
||||||
|
with the ".throttle_scope` property of the view.
|
||||||
|
"""
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
ident = request.user.id
|
||||||
|
else:
|
||||||
|
ident = request.META.get("REMOTE_ADDR", None)
|
||||||
|
|
||||||
|
return self.cache_format % {
|
||||||
|
"scope": self.scope,
|
||||||
|
"ident": ident
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
|
||||||
|
from django.core.urlresolvers import RegexURLResolver
|
||||||
|
from django.conf.urls import patterns, url, include
|
||||||
|
|
||||||
|
from .settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
|
||||||
|
ret = []
|
||||||
|
for urlpattern in urlpatterns:
|
||||||
|
if isinstance(urlpattern, RegexURLResolver):
|
||||||
|
# Set of included URL patterns
|
||||||
|
regex = urlpattern.regex.pattern
|
||||||
|
namespace = urlpattern.namespace
|
||||||
|
app_name = urlpattern.app_name
|
||||||
|
kwargs = urlpattern.default_kwargs
|
||||||
|
# Add in the included patterns, after applying the suffixes
|
||||||
|
patterns = apply_suffix_patterns(urlpattern.url_patterns,
|
||||||
|
suffix_pattern,
|
||||||
|
suffix_required)
|
||||||
|
ret.append(url(regex, include(patterns, namespace, app_name), kwargs))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Regular URL pattern
|
||||||
|
regex = urlpattern.regex.pattern.rstrip("$") + suffix_pattern
|
||||||
|
view = urlpattern._callback or urlpattern._callback_str
|
||||||
|
kwargs = urlpattern.default_args
|
||||||
|
name = urlpattern.name
|
||||||
|
# Add in both the existing and the new urlpattern
|
||||||
|
if not suffix_required:
|
||||||
|
ret.append(urlpattern)
|
||||||
|
ret.append(url(regex, view, kwargs, name))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||||
|
"""
|
||||||
|
Supplement existing urlpatterns with corresponding patterns that also
|
||||||
|
include a ".format" suffix. Retains urlpattern ordering.
|
||||||
|
|
||||||
|
urlpatterns:
|
||||||
|
A list of URL patterns.
|
||||||
|
|
||||||
|
suffix_required:
|
||||||
|
If `True`, only suffixed URLs will be generated, and non-suffixed
|
||||||
|
URLs will not be used. Defaults to `False`.
|
||||||
|
|
||||||
|
allowed:
|
||||||
|
An optional tuple/list of allowed suffixes. eg ["json", "api"]
|
||||||
|
Defaults to `None`, which allows any suffix.
|
||||||
|
"""
|
||||||
|
suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG
|
||||||
|
if allowed:
|
||||||
|
if len(allowed) == 1:
|
||||||
|
allowed_pattern = allowed[0]
|
||||||
|
else:
|
||||||
|
allowed_pattern = "(%s)" % "|".join(allowed)
|
||||||
|
suffix_pattern = r"\.(?P<%s>%s)$" % (suffix_kwarg, allowed_pattern)
|
||||||
|
else:
|
||||||
|
suffix_pattern = r"\.(?P<%s>[a-z0-9]+)$" % suffix_kwarg
|
||||||
|
|
||||||
|
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Login and logout views for the browsable API.
|
||||||
|
|
||||||
|
Add these to your root URLconf if you're using the browsable API and
|
||||||
|
your API requires authentication.
|
||||||
|
|
||||||
|
The urls must be namespaced as 'api', and you should make sure
|
||||||
|
your authentication settings include `SessionAuthentication`.
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
...
|
||||||
|
url(r'^auth', include('taiga.base.api.urls', namespace='api'))
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
from django.conf.urls import patterns
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
|
||||||
|
template_name = {"template_name": "api/login.html"}
|
||||||
|
|
||||||
|
urlpatterns = patterns("django.contrib.auth.views",
|
||||||
|
url(r"^login/$", "login", template_name, name="login"),
|
||||||
|
url(r"^logout/$", "logout", template_name, name="logout"),
|
||||||
|
)
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
||||||
|
"""
|
||||||
|
Same as Django's standard shortcut, but make sure to raise 404
|
||||||
|
if the filter_kwargs don't match the required types.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise Http404
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
from django.core.urlresolvers import resolve, get_script_prefix
|
||||||
|
|
||||||
|
|
||||||
|
def get_breadcrumbs(url):
|
||||||
|
"""
|
||||||
|
Given a url returns a list of breadcrumbs, which are each a
|
||||||
|
tuple of (name, url).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from taiga.base.api.settings import api_settings
|
||||||
|
from taiga.base.api.views import APIView
|
||||||
|
|
||||||
|
view_name_func = api_settings.VIEW_NAME_FUNCTION
|
||||||
|
|
||||||
|
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
|
||||||
|
"""
|
||||||
|
Add tuples of (name, url) to the breadcrumbs list,
|
||||||
|
progressively chomping off parts of the url.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
(view, unused_args, unused_kwargs) = resolve(url)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Check if this is a REST framework view,
|
||||||
|
# and if so add it to the breadcrumbs
|
||||||
|
cls = getattr(view, "cls", None)
|
||||||
|
if cls is not None and issubclass(cls, APIView):
|
||||||
|
# Don't list the same view twice in a row.
|
||||||
|
# Probably an optional trailing slash.
|
||||||
|
if not seen or seen[-1] != view:
|
||||||
|
suffix = getattr(view, "suffix", None)
|
||||||
|
name = view_name_func(cls, suffix)
|
||||||
|
breadcrumbs_list.insert(0, (name, prefix + url))
|
||||||
|
seen.append(view)
|
||||||
|
|
||||||
|
if url == "":
|
||||||
|
# All done
|
||||||
|
return breadcrumbs_list
|
||||||
|
|
||||||
|
elif url.endswith("/"):
|
||||||
|
# Drop trailing slash off the end and continue to try to
|
||||||
|
# resolve more breadcrumbs
|
||||||
|
url = url.rstrip("/")
|
||||||
|
return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen)
|
||||||
|
|
||||||
|
# Drop trailing non-slash off the end and continue to try to
|
||||||
|
# resolve more breadcrumbs
|
||||||
|
url = url[:url.rfind("/") + 1]
|
||||||
|
return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen)
|
||||||
|
|
||||||
|
prefix = get_script_prefix().rstrip("/")
|
||||||
|
url = url[len(prefix):]
|
||||||
|
return breadcrumbs_recursive(url, [], prefix, [])
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Helper classes for parsers.
|
||||||
|
"""
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
|
from django.utils.functional import Promise
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import decimal
|
||||||
|
import types
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class JSONEncoder(json.JSONEncoder):
|
||||||
|
"""
|
||||||
|
JSONEncoder subclass that knows how to encode date/time/timedelta,
|
||||||
|
decimal types, and generators.
|
||||||
|
"""
|
||||||
|
def default(self, o):
|
||||||
|
# For Date Time string spec, see ECMA 262
|
||||||
|
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||||
|
if isinstance(o, Promise):
|
||||||
|
return force_text(o)
|
||||||
|
elif isinstance(o, datetime.datetime):
|
||||||
|
r = o.isoformat()
|
||||||
|
if o.microsecond:
|
||||||
|
r = r[:23] + r[26:]
|
||||||
|
if r.endswith("+00:00"):
|
||||||
|
r = r[:-6] + "Z"
|
||||||
|
return r
|
||||||
|
elif isinstance(o, datetime.date):
|
||||||
|
return o.isoformat()
|
||||||
|
elif isinstance(o, datetime.time):
|
||||||
|
if timezone and timezone.is_aware(o):
|
||||||
|
raise ValueError("JSON can't represent timezone-aware times.")
|
||||||
|
r = o.isoformat()
|
||||||
|
if o.microsecond:
|
||||||
|
r = r[:12]
|
||||||
|
return r
|
||||||
|
elif isinstance(o, datetime.timedelta):
|
||||||
|
return str(o.total_seconds())
|
||||||
|
elif isinstance(o, decimal.Decimal):
|
||||||
|
return str(o)
|
||||||
|
elif isinstance(o, QuerySet):
|
||||||
|
return list(o)
|
||||||
|
elif hasattr(o, "tolist"):
|
||||||
|
return o.tolist()
|
||||||
|
elif hasattr(o, "__getitem__"):
|
||||||
|
try:
|
||||||
|
return dict(o)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif hasattr(o, "__iter__"):
|
||||||
|
return [i for i in o]
|
||||||
|
return super(JSONEncoder, self).default(o)
|
||||||
|
|
||||||
|
|
||||||
|
SafeDumper = None
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility functions to return a formatted name and description for a given view.
|
||||||
|
"""
|
||||||
|
from django.utils.html import escape
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from taiga.base.api.settings import api_settings
|
||||||
|
|
||||||
|
from textwrap import dedent
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Markdown is optional
|
||||||
|
try:
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
def apply_markdown(text):
|
||||||
|
"""
|
||||||
|
Simple wrapper around :func:`markdown.markdown` to set the base level
|
||||||
|
of '#' style headers to <h2>.
|
||||||
|
"""
|
||||||
|
extensions = ["headerid(level=2)"]
|
||||||
|
safe_mode = False
|
||||||
|
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
|
||||||
|
return md.convert(text)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
apply_markdown = None
|
||||||
|
|
||||||
|
|
||||||
|
def remove_trailing_string(content, trailing):
|
||||||
|
"""
|
||||||
|
Strip trailing component `trailing` from `content` if it exists.
|
||||||
|
Used when generating names from view classes.
|
||||||
|
"""
|
||||||
|
if content.endswith(trailing) and content != trailing:
|
||||||
|
return content[:-len(trailing)]
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def dedent(content):
|
||||||
|
"""
|
||||||
|
Remove leading indent from a block of text.
|
||||||
|
Used when generating descriptions from docstrings.
|
||||||
|
|
||||||
|
Note that python's `textwrap.dedent` doesn't quite cut it,
|
||||||
|
as it fails to dedent multiline docstrings that include
|
||||||
|
unindented text on the initial line.
|
||||||
|
"""
|
||||||
|
whitespace_counts = [len(line) - len(line.lstrip(" "))
|
||||||
|
for line in content.splitlines()[1:] if line.lstrip()]
|
||||||
|
|
||||||
|
# unindent the content if needed
|
||||||
|
if whitespace_counts:
|
||||||
|
whitespace_pattern = "^" + (" " * min(whitespace_counts))
|
||||||
|
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), "", content)
|
||||||
|
|
||||||
|
return content.strip()
|
||||||
|
|
||||||
|
def camelcase_to_spaces(content):
|
||||||
|
"""
|
||||||
|
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
||||||
|
Used when generating names from view classes.
|
||||||
|
"""
|
||||||
|
camelcase_boundry = "(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))"
|
||||||
|
content = re.sub(camelcase_boundry, " \\1", content).strip()
|
||||||
|
return " ".join(content.split("_")).title()
|
||||||
|
|
||||||
|
def markup_description(description):
|
||||||
|
"""
|
||||||
|
Apply HTML markup to the given description.
|
||||||
|
"""
|
||||||
|
if apply_markdown:
|
||||||
|
description = apply_markdown(description)
|
||||||
|
else:
|
||||||
|
description = escape(description).replace("\n", "<br />")
|
||||||
|
return mark_safe(description)
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handling of media types, as found in HTTP Content-Type and Accept headers.
|
||||||
|
|
||||||
|
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
||||||
|
"""
|
||||||
|
from django.http.multipartparser import parse_header
|
||||||
|
|
||||||
|
from taiga.base.api import HTTP_HEADER_ENCODING
|
||||||
|
|
||||||
|
|
||||||
|
def media_type_matches(lhs, rhs):
|
||||||
|
"""
|
||||||
|
Returns ``True`` if the media type in the first argument <= the
|
||||||
|
media type in the second argument. The media types are strings
|
||||||
|
as described by the HTTP spec.
|
||||||
|
|
||||||
|
Valid media type strings include:
|
||||||
|
|
||||||
|
'application/json; indent=4'
|
||||||
|
'application/json'
|
||||||
|
'text/*'
|
||||||
|
'*/*'
|
||||||
|
"""
|
||||||
|
lhs = _MediaType(lhs)
|
||||||
|
rhs = _MediaType(rhs)
|
||||||
|
return lhs.match(rhs)
|
||||||
|
|
||||||
|
|
||||||
|
def order_by_precedence(media_type_lst):
|
||||||
|
"""
|
||||||
|
Returns a list of sets of media type strings, ordered by precedence.
|
||||||
|
Precedence is determined by how specific a media type is:
|
||||||
|
|
||||||
|
3. 'type/subtype; param=val'
|
||||||
|
2. 'type/subtype'
|
||||||
|
1. 'type/*'
|
||||||
|
0. '*/*'
|
||||||
|
"""
|
||||||
|
ret = [set(), set(), set(), set()]
|
||||||
|
for media_type in media_type_lst:
|
||||||
|
precedence = _MediaType(media_type).precedence
|
||||||
|
ret[3 - precedence].add(media_type)
|
||||||
|
return [media_types for media_types in ret if media_types]
|
||||||
|
|
||||||
|
|
||||||
|
class _MediaType(object):
|
||||||
|
def __init__(self, media_type_str):
|
||||||
|
if media_type_str is None:
|
||||||
|
media_type_str = ''
|
||||||
|
self.orig = media_type_str
|
||||||
|
self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING))
|
||||||
|
self.main_type, sep, self.sub_type = self.full_type.partition("/")
|
||||||
|
|
||||||
|
def match(self, other):
|
||||||
|
"""Return true if this MediaType satisfies the given MediaType."""
|
||||||
|
for key in self.params.keys():
|
||||||
|
if key != "q" and other.params.get(key, None) != self.params.get(key, None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.sub_type != "*" and other.sub_type != "*" and other.sub_type != self.sub_type:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.main_type != "*" and other.main_type != "*" and other.main_type != self.main_type:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def precedence(self):
|
||||||
|
"""
|
||||||
|
Return a precedence level from 0-3 for the media type given how specific it is.
|
||||||
|
"""
|
||||||
|
if self.main_type == "*":
|
||||||
|
return 0
|
||||||
|
elif self.sub_type == "*":
|
||||||
|
return 1
|
||||||
|
elif not self.params or self.params.keys() == ["q"]:
|
||||||
|
return 2
|
||||||
|
return 3
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return unicode(self).encode("utf-8")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
ret = "%s/%s" % (self.main_type, self.sub_type)
|
||||||
|
for key, val in self.params.items():
|
||||||
|
ret += "; %s=%s" % (key, val)
|
||||||
|
return ret
|
|
@ -19,26 +19,29 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import Http404, HttpResponse
|
from django.http import Http404, HttpResponse
|
||||||
from django.utils.datastructures import SortedDict
|
from django.http.response import HttpResponseBase
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.defaults import server_error
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.utils.datastructures import SortedDict
|
||||||
|
from django.utils.encoding import smart_text
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import status, exceptions
|
from .request import Request
|
||||||
from rest_framework.compat import smart_text, HttpResponseBase, View
|
from .settings import api_settings
|
||||||
from rest_framework.request import Request
|
from .utils import formatting
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
from rest_framework.utils import formatting
|
|
||||||
|
|
||||||
|
from taiga.base import status
|
||||||
|
from taiga.base import exceptions
|
||||||
from taiga.base.response import Response
|
from taiga.base.response import Response
|
||||||
from taiga.base.response import Ok
|
from taiga.base.response import Ok
|
||||||
from taiga.base.response import NotFound
|
from taiga.base.response import NotFound
|
||||||
from taiga.base.response import Forbidden
|
from taiga.base.response import Forbidden
|
||||||
from taiga.base.utils.iterators import as_tuple
|
from taiga.base.utils.iterators import as_tuple
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.views.defaults import server_error
|
|
||||||
|
|
||||||
|
|
||||||
def get_view_name(view_cls, suffix=None):
|
def get_view_name(view_cls, suffix=None):
|
||||||
|
@ -293,7 +296,7 @@ class APIView(View):
|
||||||
"""
|
"""
|
||||||
request.user
|
request.user
|
||||||
|
|
||||||
def check_permissions(self, request, action, obj=None):
|
def check_permissions(self, request, action:str=None, obj=None):
|
||||||
if action is None:
|
if action is None:
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ from django.utils.translation import ugettext as _
|
||||||
from . import views
|
from . import views
|
||||||
from . import mixins
|
from . import mixins
|
||||||
from . import generics
|
from . import generics
|
||||||
from . import pagination
|
|
||||||
|
|
||||||
|
|
||||||
class ViewSetMixin(object):
|
class ViewSetMixin(object):
|
||||||
|
@ -126,9 +125,7 @@ class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyListViewSet(pagination.HeadersPaginationMixin,
|
class ReadOnlyListViewSet(GenericViewSet):
|
||||||
pagination.ConditionalPaginationMixin,
|
|
||||||
GenericViewSet):
|
|
||||||
"""
|
"""
|
||||||
A viewset that provides default `list()` action.
|
A viewset that provides default `list()` action.
|
||||||
"""
|
"""
|
||||||
|
@ -157,15 +154,11 @@ class ModelViewSet(mixins.CreateModelMixin,
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModelCrudViewSet(pagination.HeadersPaginationMixin,
|
class ModelCrudViewSet(ModelViewSet):
|
||||||
pagination.ConditionalPaginationMixin,
|
|
||||||
ModelViewSet):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModelListViewSet(pagination.HeadersPaginationMixin,
|
class ModelListViewSet(mixins.RetrieveModelMixin,
|
||||||
pagination.ConditionalPaginationMixin,
|
|
||||||
mixins.RetrieveModelMixin,
|
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet):
|
GenericViewSet):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -13,19 +13,10 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import sys
|
|
||||||
|
|
||||||
# Patch api view for correctly return 401 responses on
|
|
||||||
# request is authenticated instead of 403
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from . import monkey
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAppConfig(AppConfig):
|
class BaseAppConfig(AppConfig):
|
||||||
name = "taiga.base"
|
name = "taiga.base"
|
||||||
verbose_name = "Base App Config"
|
verbose_name = "Base App Config"
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
print("Monkey patching...", file=sys.stderr)
|
|
||||||
monkey.patch_restframework()
|
|
||||||
monkey.patch_serializer()
|
|
||||||
|
|
|
@ -14,18 +14,101 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import exceptions
|
# This code is partially taken from django-rest-framework:
|
||||||
from rest_framework import status
|
# Copyright (c) 2011-2015, Tom Christie
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handled exceptions raised by REST framework.
|
||||||
|
|
||||||
|
In addition Django's built in 403 and 404 exceptions are handled.
|
||||||
|
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
|
||||||
|
"""
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
|
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
from taiga.base import response
|
from . import response
|
||||||
|
from . import status
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
class BaseException(exceptions.APIException):
|
class APIException(Exception):
|
||||||
|
"""
|
||||||
|
Base class for REST framework exceptions.
|
||||||
|
Subclasses should provide `.status_code` and `.default_detail` properties.
|
||||||
|
"""
|
||||||
|
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
default_detail = ""
|
||||||
|
|
||||||
|
def __init__(self, detail=None):
|
||||||
|
self.detail = detail or self.default_detail
|
||||||
|
|
||||||
|
|
||||||
|
class ParseError(APIException):
|
||||||
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
default_detail = _("Malformed request.")
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationFailed(APIException):
|
||||||
|
status_code = status.HTTP_401_UNAUTHORIZED
|
||||||
|
default_detail = _("Incorrect authentication credentials.")
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthenticated(APIException):
|
||||||
|
status_code = status.HTTP_401_UNAUTHORIZED
|
||||||
|
default_detail = _("Authentication credentials were not provided.")
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionDenied(APIException):
|
||||||
|
status_code = status.HTTP_403_FORBIDDEN
|
||||||
|
default_detail = _("You do not have permission to perform this action.")
|
||||||
|
|
||||||
|
|
||||||
|
class MethodNotAllowed(APIException):
|
||||||
|
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
|
||||||
|
default_detail = _("Method '%s' not allowed.")
|
||||||
|
|
||||||
|
def __init__(self, method, detail=None):
|
||||||
|
self.detail = (detail or self.default_detail) % method
|
||||||
|
|
||||||
|
|
||||||
|
class NotAcceptable(APIException):
|
||||||
|
status_code = status.HTTP_406_NOT_ACCEPTABLE
|
||||||
|
default_detail = _("Could not satisfy the request's Accept header")
|
||||||
|
|
||||||
|
def __init__(self, detail=None, available_renderers=None):
|
||||||
|
self.detail = detail or self.default_detail
|
||||||
|
self.available_renderers = available_renderers
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedMediaType(APIException):
|
||||||
|
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||||
|
default_detail = _("Unsupported media type '%s' in request.")
|
||||||
|
|
||||||
|
def __init__(self, media_type, detail=None):
|
||||||
|
self.detail = (detail or self.default_detail) % media_type
|
||||||
|
|
||||||
|
|
||||||
|
class Throttled(APIException):
|
||||||
|
status_code = status.HTTP_429_TOO_MANY_REQUESTS
|
||||||
|
default_detail = _("Request was throttled.")
|
||||||
|
extra_detail = _("Expected available in %d second%s.")
|
||||||
|
|
||||||
|
def __init__(self, wait=None, detail=None):
|
||||||
|
if wait is None:
|
||||||
|
self.detail = detail or self.default_detail
|
||||||
|
self.wait = None
|
||||||
|
else:
|
||||||
|
format = "%s%s" % ((detail or self.default_detail), self.extra_detail)
|
||||||
|
self.detail = format % (wait, wait != 1 and "s" or "")
|
||||||
|
self.wait = math.ceil(wait)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseException(APIException):
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
default_detail = _("Unexpected error")
|
default_detail = _("Unexpected error")
|
||||||
|
|
||||||
|
@ -67,7 +150,7 @@ class RequestValidationError(BadRequest):
|
||||||
default_detail = _("Data validation error")
|
default_detail = _("Data validation error")
|
||||||
|
|
||||||
|
|
||||||
class PermissionDenied(exceptions.PermissionDenied):
|
class PermissionDenied(PermissionDenied):
|
||||||
"""
|
"""
|
||||||
Compatibility subclass of restframework `PermissionDenied`
|
Compatibility subclass of restframework `PermissionDenied`
|
||||||
exception.
|
exception.
|
||||||
|
@ -86,7 +169,7 @@ class PreconditionError(BadRequest):
|
||||||
default_detail = _("Precondition error")
|
default_detail = _("Precondition error")
|
||||||
|
|
||||||
|
|
||||||
class NotAuthenticated(exceptions.NotAuthenticated):
|
class NotAuthenticated(NotAuthenticated):
|
||||||
"""
|
"""
|
||||||
Compatibility subclass of restframework `NotAuthenticated`
|
Compatibility subclass of restframework `NotAuthenticated`
|
||||||
exception.
|
exception.
|
||||||
|
@ -119,7 +202,7 @@ def exception_handler(exc):
|
||||||
to be raised.
|
to be raised.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(exc, exceptions.APIException):
|
if isinstance(exc, APIException):
|
||||||
headers = {}
|
headers = {}
|
||||||
if getattr(exc, "auth_header", None):
|
if getattr(exc, "auth_header", None):
|
||||||
headers["WWW-Authenticate"] = exc.auth_header
|
headers["WWW-Authenticate"] = exc.auth_header
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.forms import widgets
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################
|
||||||
|
## Serializer fields
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
class JsonField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
Json objects serializer.
|
||||||
|
"""
|
||||||
|
widget = widgets.Textarea
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class PgArrayField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
PgArray objects serializer.
|
||||||
|
"""
|
||||||
|
widget = widgets.Textarea
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class TagsField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
Pickle objects serializer.
|
||||||
|
"""
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
if not data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
ret = sum([tag.split(",") for tag in data], [])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class TagsColorsField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
PgArray objects serializer.
|
||||||
|
"""
|
||||||
|
widget = widgets.Textarea
|
||||||
|
|
||||||
|
def to_native(self, obj):
|
||||||
|
return dict(obj)
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
return list(data.items())
|
|
@ -21,8 +21,6 @@ from django.apps import apps
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import filters
|
|
||||||
|
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
|
@ -30,7 +28,20 @@ from taiga.base.api.utils import get_object_or_404
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class QueryParamsFilterMixin(filters.BaseFilterBackend):
|
|
||||||
|
class BaseFilterBackend(object):
|
||||||
|
"""
|
||||||
|
A base class from which all filter backend classes should inherit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
"""
|
||||||
|
Return a filtered queryset.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(".filter_queryset() must be overridden.")
|
||||||
|
|
||||||
|
|
||||||
|
class QueryParamsFilterMixin(BaseFilterBackend):
|
||||||
_special_values_dict = {
|
_special_values_dict = {
|
||||||
'true': True,
|
'true': True,
|
||||||
'false': False,
|
'false': False,
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
|
||||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
|
|
||||||
def patch_serializer():
|
|
||||||
from rest_framework import serializers
|
|
||||||
if hasattr(serializers.BaseSerializer, "_patched"):
|
|
||||||
return
|
|
||||||
|
|
||||||
def to_native(self, obj):
|
|
||||||
"""
|
|
||||||
Serialize objects -> primitives.
|
|
||||||
"""
|
|
||||||
ret = self._dict_class()
|
|
||||||
ret.fields = self._dict_class()
|
|
||||||
ret.empty = obj is None
|
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
|
||||||
field.initialize(parent=self, field_name=field_name)
|
|
||||||
key = self.get_field_key(field_name)
|
|
||||||
ret.fields[key] = field
|
|
||||||
|
|
||||||
if obj is not None:
|
|
||||||
value = field.field_to_native(obj, field_name)
|
|
||||||
ret[key] = value
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
serializers.BaseSerializer._patched = True
|
|
||||||
serializers.BaseSerializer.to_native = to_native
|
|
||||||
|
|
||||||
|
|
||||||
def patch_restframework():
|
|
||||||
from rest_framework import fields
|
|
||||||
fields.strip_multiple_choice_msg = lambda x: x
|
|
|
@ -19,6 +19,8 @@ from collections import namedtuple
|
||||||
|
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
Neighbor = namedtuple("Neighbor", "left right")
|
Neighbor = namedtuple("Neighbor", "left right")
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,3 +69,25 @@ def get_neighbors(obj, results_set=None):
|
||||||
right = None
|
right = None
|
||||||
|
|
||||||
return Neighbor(left, right)
|
return Neighbor(left, right)
|
||||||
|
|
||||||
|
|
||||||
|
class NeighborsSerializerMixin:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors")
|
||||||
|
|
||||||
|
def serialize_neighbor(self, neighbor):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_neighbors(self, obj):
|
||||||
|
view, request = self.context.get("view", None), self.context.get("request", None)
|
||||||
|
if view and request:
|
||||||
|
queryset = view.filter_queryset(view.get_queryset())
|
||||||
|
left, right = get_neighbors(obj, results_set=queryset)
|
||||||
|
else:
|
||||||
|
left = right = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"previous": self.serialize_neighbor(left),
|
||||||
|
"next": self.serialize_neighbor(right)
|
||||||
|
}
|
||||||
|
|
|
@ -15,17 +15,92 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2014, Tom Christie
|
||||||
|
|
||||||
"""The various HTTP responses for use in returning proper HTTP codes."""
|
"""The various HTTP responses for use in returning proper HTTP codes."""
|
||||||
from django import http
|
from django import http
|
||||||
|
|
||||||
import rest_framework.response
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
|
from django.template.response import SimpleTemplateResponse
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
|
||||||
class Response(rest_framework.response.Response):
|
class Response(SimpleTemplateResponse):
|
||||||
def __init__(self, data=None, status=None, template_name=None, headers=None, exception=False,
|
"""
|
||||||
content_type=None):
|
An HttpResponse that allows its data to be rendered into
|
||||||
super(Response, self).__init__(data, status, template_name, headers, exception,
|
arbitrary media types.
|
||||||
content_type)
|
"""
|
||||||
|
def __init__(self, data=None, status=None,
|
||||||
|
template_name=None, headers=None,
|
||||||
|
exception=False, content_type=None):
|
||||||
|
"""
|
||||||
|
Alters the init arguments slightly.
|
||||||
|
For example, drop 'template_name', and instead use 'data'.
|
||||||
|
|
||||||
|
Setting 'renderer' and 'media_type' will typically be deferred,
|
||||||
|
For example being set automatically by the `APIView`.
|
||||||
|
"""
|
||||||
|
super().__init__(None, status=status)
|
||||||
|
self.data = data
|
||||||
|
self.template_name = template_name
|
||||||
|
self.exception = exception
|
||||||
|
self.content_type = content_type
|
||||||
|
|
||||||
|
if headers:
|
||||||
|
for name, value in six.iteritems(headers):
|
||||||
|
self[name] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rendered_content(self):
|
||||||
|
renderer = getattr(self, "accepted_renderer", None)
|
||||||
|
media_type = getattr(self, "accepted_media_type", None)
|
||||||
|
context = getattr(self, "renderer_context", None)
|
||||||
|
|
||||||
|
assert renderer, ".accepted_renderer not set on Response"
|
||||||
|
assert media_type, ".accepted_media_type not set on Response"
|
||||||
|
assert context, ".renderer_context not set on Response"
|
||||||
|
context["response"] = self
|
||||||
|
|
||||||
|
charset = renderer.charset
|
||||||
|
content_type = self.content_type
|
||||||
|
|
||||||
|
if content_type is None and charset is not None:
|
||||||
|
content_type = "{0}; charset={1}".format(media_type, charset)
|
||||||
|
elif content_type is None:
|
||||||
|
content_type = media_type
|
||||||
|
self["Content-Type"] = content_type
|
||||||
|
|
||||||
|
ret = renderer.render(self.data, media_type, context)
|
||||||
|
if isinstance(ret, six.text_type):
|
||||||
|
assert charset, "renderer returned unicode, and did not specify " \
|
||||||
|
"a charset value."
|
||||||
|
return bytes(ret.encode(charset))
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
del self["Content-Type"]
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_text(self):
|
||||||
|
"""
|
||||||
|
Returns reason text corresponding to our HTTP response status code.
|
||||||
|
Provided for convenience.
|
||||||
|
"""
|
||||||
|
# TODO: Deprecate and use a template tag instead
|
||||||
|
# TODO: Status code text for RFC 6585 status codes
|
||||||
|
return STATUS_CODE_TEXT.get(self.status_code, '')
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
"""
|
||||||
|
Remove attributes from the response that shouldn't be cached
|
||||||
|
"""
|
||||||
|
state = super().__getstate__()
|
||||||
|
for key in ("accepted_renderer", "renderer_context", "data"):
|
||||||
|
if key in state:
|
||||||
|
del state[key]
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
class Ok(Response):
|
class Ok(Response):
|
||||||
|
|
|
@ -22,10 +22,10 @@ from django.conf.urls import patterns, url
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
from django.core.urlresolvers import NoReverseMatch
|
||||||
|
|
||||||
from rest_framework import views
|
from taiga.base.api import views
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from rest_framework.reverse import reverse
|
from taiga.base.api.reverse import reverse
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from taiga.base.api.urlpatterns import format_suffix_patterns
|
||||||
|
|
||||||
|
|
||||||
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
|
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
|
||||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as
|
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
|
||||||
# License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from django.forms import widgets
|
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from .neighbors import get_neighbors
|
|
||||||
|
|
||||||
|
|
||||||
class TagsField(serializers.WritableField):
|
|
||||||
"""
|
|
||||||
Pickle objects serializer.
|
|
||||||
"""
|
|
||||||
def to_native(self, obj):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def from_native(self, data):
|
|
||||||
if not data:
|
|
||||||
return data
|
|
||||||
|
|
||||||
ret = sum([tag.split(",") for tag in data], [])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class JsonField(serializers.WritableField):
|
|
||||||
"""
|
|
||||||
Json objects serializer.
|
|
||||||
"""
|
|
||||||
widget = widgets.Textarea
|
|
||||||
|
|
||||||
def to_native(self, obj):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def from_native(self, data):
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class PgArrayField(serializers.WritableField):
|
|
||||||
"""
|
|
||||||
PgArray objects serializer.
|
|
||||||
"""
|
|
||||||
widget = widgets.Textarea
|
|
||||||
|
|
||||||
def to_native(self, obj):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def from_native(self, data):
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class TagsColorsField(serializers.WritableField):
|
|
||||||
"""
|
|
||||||
PgArray objects serializer.
|
|
||||||
"""
|
|
||||||
widget = widgets.Textarea
|
|
||||||
|
|
||||||
def to_native(self, obj):
|
|
||||||
return dict(obj)
|
|
||||||
|
|
||||||
def from_native(self, data):
|
|
||||||
return list(data.items())
|
|
||||||
|
|
||||||
|
|
||||||
class NeighborsSerializerMixin:
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors")
|
|
||||||
|
|
||||||
def serialize_neighbor(self, neighbor):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_neighbors(self, obj):
|
|
||||||
view, request = self.context.get("view", None), self.context.get("request", None)
|
|
||||||
if view and request:
|
|
||||||
queryset = view.filter_queryset(view.get_queryset())
|
|
||||||
left, right = get_neighbors(obj, results_set=queryset)
|
|
||||||
else:
|
|
||||||
left = right = None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"previous": self.serialize_neighbor(left),
|
|
||||||
"next": self.serialize_neighbor(right)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Serializer(serializers.Serializer):
|
|
||||||
def skip_field_validation(self, field, attrs, source):
|
|
||||||
return source not in attrs and (field.partial or not field.required)
|
|
||||||
|
|
||||||
def perform_validation(self, attrs):
|
|
||||||
"""
|
|
||||||
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
|
||||||
"""
|
|
||||||
for field_name, field in self.fields.items():
|
|
||||||
if field_name in self._errors:
|
|
||||||
continue
|
|
||||||
|
|
||||||
source = field.source or field_name
|
|
||||||
if self.skip_field_validation(field, attrs, source):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
|
||||||
if validate_method:
|
|
||||||
attrs = validate_method(attrs, source)
|
|
||||||
except serializers.ValidationError as err:
|
|
||||||
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
|
||||||
|
|
||||||
# If there are already errors, we don't run .validate() because
|
|
||||||
# field-validation failed and thus `attrs` may not be complete.
|
|
||||||
# which in turn can cause inconsistent validation errors.
|
|
||||||
if not self._errors:
|
|
||||||
try:
|
|
||||||
attrs = self.validate(attrs)
|
|
||||||
except serializers.ValidationError as err:
|
|
||||||
if hasattr(err, 'message_dict'):
|
|
||||||
for field_name, error_messages in err.message_dict.items():
|
|
||||||
self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
|
|
||||||
elif hasattr(err, 'messages'):
|
|
||||||
self._errors['non_field_errors'] = err.messages
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializer(serializers.ModelSerializer):
|
|
||||||
def perform_validation(self, attrs):
|
|
||||||
for attr in attrs:
|
|
||||||
field = self.fields.get(attr, None)
|
|
||||||
if field:
|
|
||||||
field.required = True
|
|
||||||
return super().perform_validation(attrs)
|
|
||||||
|
|
||||||
def save(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Due to DRF bug with M2M fields we refresh object state from database
|
|
||||||
directly if object is models.Model type and it contains m2m fields
|
|
||||||
|
|
||||||
See: https://github.com/tomchristie/django-rest-framework/issues/1556
|
|
||||||
"""
|
|
||||||
self.object = super(serializers.ModelSerializer, self).save(**kwargs)
|
|
||||||
model = self.Meta.model
|
|
||||||
if model._meta.model._meta.local_many_to_many and self.object.pk:
|
|
||||||
self.object = model.objects.get(pk=self.object.pk)
|
|
||||||
return self.object
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This code is partially taken from django-rest-framework:
|
||||||
|
# Copyright (c) 2011-2015, Tom Christie
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Descriptive HTTP status codes, for code readability.
|
||||||
|
|
||||||
|
See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||||
|
And RFC 6585 - http://tools.ietf.org/html/rfc6585
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def is_informational(code):
|
||||||
|
return code >= 100 and code <= 199
|
||||||
|
|
||||||
|
def is_success(code):
|
||||||
|
return code >= 200 and code <= 299
|
||||||
|
|
||||||
|
def is_redirect(code):
|
||||||
|
return code >= 300 and code <= 399
|
||||||
|
|
||||||
|
def is_client_error(code):
|
||||||
|
return code >= 400 and code <= 499
|
||||||
|
|
||||||
|
def is_server_error(code):
|
||||||
|
return code >= 500 and code <= 599
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_100_CONTINUE = 100
|
||||||
|
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||||
|
HTTP_200_OK = 200
|
||||||
|
HTTP_201_CREATED = 201
|
||||||
|
HTTP_202_ACCEPTED = 202
|
||||||
|
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
|
||||||
|
HTTP_204_NO_CONTENT = 204
|
||||||
|
HTTP_205_RESET_CONTENT = 205
|
||||||
|
HTTP_206_PARTIAL_CONTENT = 206
|
||||||
|
HTTP_300_MULTIPLE_CHOICES = 300
|
||||||
|
HTTP_301_MOVED_PERMANENTLY = 301
|
||||||
|
HTTP_302_FOUND = 302
|
||||||
|
HTTP_303_SEE_OTHER = 303
|
||||||
|
HTTP_304_NOT_MODIFIED = 304
|
||||||
|
HTTP_305_USE_PROXY = 305
|
||||||
|
HTTP_306_RESERVED = 306
|
||||||
|
HTTP_307_TEMPORARY_REDIRECT = 307
|
||||||
|
HTTP_400_BAD_REQUEST = 400
|
||||||
|
HTTP_401_UNAUTHORIZED = 401
|
||||||
|
HTTP_402_PAYMENT_REQUIRED = 402
|
||||||
|
HTTP_403_FORBIDDEN = 403
|
||||||
|
HTTP_404_NOT_FOUND = 404
|
||||||
|
HTTP_405_METHOD_NOT_ALLOWED = 405
|
||||||
|
HTTP_406_NOT_ACCEPTABLE = 406
|
||||||
|
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
|
||||||
|
HTTP_408_REQUEST_TIMEOUT = 408
|
||||||
|
HTTP_409_CONFLICT = 409
|
||||||
|
HTTP_410_GONE = 410
|
||||||
|
HTTP_411_LENGTH_REQUIRED = 411
|
||||||
|
HTTP_412_PRECONDITION_FAILED = 412
|
||||||
|
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
|
||||||
|
HTTP_414_REQUEST_URI_TOO_LONG = 414
|
||||||
|
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
|
||||||
|
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
|
||||||
|
HTTP_417_EXPECTATION_FAILED = 417
|
||||||
|
HTTP_428_PRECONDITION_REQUIRED = 428
|
||||||
|
HTTP_429_TOO_MANY_REQUESTS = 429
|
||||||
|
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||||
|
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
||||||
|
HTTP_501_NOT_IMPLEMENTED = 501
|
||||||
|
HTTP_502_BAD_GATEWAY = 502
|
||||||
|
HTTP_503_SERVICE_UNAVAILABLE = 503
|
||||||
|
HTTP_504_GATEWAY_TIMEOUT = 504
|
||||||
|
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
||||||
|
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import throttling
|
from taiga.base.api import throttling
|
||||||
|
|
||||||
|
|
||||||
class AnonRateThrottle(throttling.AnonRateThrottle):
|
class AnonRateThrottle(throttling.AnonRateThrottle):
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
|
||||||
from rest_framework.utils import encoders
|
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
from taiga.base.api.utils import encoders
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder):
|
def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder):
|
||||||
return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii)
|
return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework.renderers import UnicodeJSONRenderer
|
from taiga.base.api.renderers import UnicodeJSONRenderer
|
||||||
|
|
||||||
|
|
||||||
class ExportRenderer(UnicodeJSONRenderer):
|
class ExportRenderer(UnicodeJSONRenderer):
|
||||||
|
|
|
@ -25,7 +25,10 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
from taiga import mdrender
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import JsonField, PgArrayField
|
||||||
|
|
||||||
from taiga.projects import models as projects_models
|
from taiga.projects import models as projects_models
|
||||||
from taiga.projects.custom_attributes import models as custom_attributes_models
|
from taiga.projects.custom_attributes import models as custom_attributes_models
|
||||||
|
@ -39,8 +42,6 @@ from taiga.projects.attachments import models as attachments_models
|
||||||
from taiga.users import models as users_models
|
from taiga.users import models as users_models
|
||||||
from taiga.projects.votes import services as votes_service
|
from taiga.projects.votes import services as votes_service
|
||||||
from taiga.projects.history import services as history_service
|
from taiga.projects.history import services as history_service
|
||||||
from taiga.base.serializers import JsonField, PgArrayField
|
|
||||||
from taiga import mdrender
|
|
||||||
|
|
||||||
|
|
||||||
class AttachedFileField(serializers.WritableField):
|
class AttachedFileField(serializers.WritableField):
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: taiga-back\n"
|
"Project-Id-Version: taiga-back\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2015-04-06 17:04+0200\n"
|
"POT-Creation-Date: 2015-04-09 20:07+0200\n"
|
||||||
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
|
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
|
||||||
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
"Last-Translator: Taiga Dev Team <support@taiga.io>\n"
|
||||||
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
"Language-Team: Taiga Dev Team <support@taiga.io>\n"
|
||||||
|
@ -16,23 +16,23 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: taiga/auth/api.py:100
|
#: taiga/auth/api.py:99
|
||||||
msgid "Public register is disabled."
|
msgid "Public register is disabled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/auth/api.py:133
|
#: taiga/auth/api.py:132
|
||||||
msgid "invalid register type"
|
msgid "invalid register type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/auth/api.py:146
|
#: taiga/auth/api.py:145
|
||||||
msgid "invalid login type"
|
msgid "invalid login type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/auth/serializers.py:33 taiga/users/serializers.py:52
|
#: taiga/auth/serializers.py:34 taiga/users/serializers.py:50
|
||||||
msgid "invalid username"
|
msgid "invalid username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/auth/serializers.py:38 taiga/users/serializers.py:58
|
#: taiga/auth/serializers.py:39 taiga/users/serializers.py:56
|
||||||
msgid ""
|
msgid ""
|
||||||
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
|
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -65,105 +65,228 @@ msgstr ""
|
||||||
msgid "Invalid token"
|
msgid "Invalid token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:129
|
#: taiga/base/api/fields.py:265
|
||||||
msgid ""
|
msgid "This field is required."
|
||||||
"The `page_size` parameter to `paginate_queryset()` is due to be deprecated. "
|
|
||||||
"Note that the return style of this method is also changed, and will simply "
|
|
||||||
"return a page object when called without a `page_size` argument."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:144 taiga/base/api/mixins.py:98
|
#: taiga/base/api/fields.py:266 taiga/base/api/relations.py:311
|
||||||
|
msgid "Invalid value."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:449
|
||||||
|
#, python-format
|
||||||
|
msgid "'%s' value must be either True or False."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:506
|
||||||
|
msgid ""
|
||||||
|
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:521
|
||||||
|
#, python-format
|
||||||
|
msgid "Select a valid choice. %(value)s is not one of the available choices."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:584
|
||||||
|
msgid "Enter a valid email address."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:626
|
||||||
|
#, python-format
|
||||||
|
msgid "Date has wrong format. Use one of these formats instead: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:690
|
||||||
|
#, python-format
|
||||||
|
msgid "Datetime has wrong format. Use one of these formats instead: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:760
|
||||||
|
#, python-format
|
||||||
|
msgid "Time has wrong format. Use one of these formats instead: %s"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:817
|
||||||
|
msgid "Enter a whole number."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:818 taiga/base/api/fields.py:871
|
||||||
|
#, python-format
|
||||||
|
msgid "Ensure this value is less than or equal to %(limit_value)s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:819 taiga/base/api/fields.py:872
|
||||||
|
#, python-format
|
||||||
|
msgid "Ensure this value is greater than or equal to %(limit_value)s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:849
|
||||||
|
#, python-format
|
||||||
|
msgid "\"%s\" value must be a float."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:870
|
||||||
|
msgid "Enter a number."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:873
|
||||||
|
#, python-format
|
||||||
|
msgid "Ensure that there are no more than %s digits in total."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:874
|
||||||
|
#, python-format
|
||||||
|
msgid "Ensure that there are no more than %s decimal places."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:875
|
||||||
|
#, python-format
|
||||||
|
msgid "Ensure that there are no more than %s digits before the decimal point."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:942
|
||||||
|
msgid "No file was submitted. Check the encoding type on the form."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:943
|
||||||
|
msgid "No file was submitted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:944
|
||||||
|
msgid "The submitted file is empty."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:945
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:946
|
||||||
|
msgid "Please either submit a file or check the clear checkbox, not both."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/fields.py:986
|
||||||
|
msgid ""
|
||||||
|
"Upload a valid image. The file you uploaded was either not an image or a "
|
||||||
|
"corrupted image."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/mixins.py:98
|
||||||
msgid ""
|
msgid ""
|
||||||
"The `allow_empty` parameter is due to be deprecated. To use "
|
"The `allow_empty` parameter is due to be deprecated. To use "
|
||||||
"`allow_empty=False` style behavior, You should override `get_queryset()` and "
|
"`allow_empty=False` style behavior, You should override `get_queryset()` and "
|
||||||
"explicitly raise a 404 on empty querysets."
|
"explicitly raise a 404 on empty querysets."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:160
|
#: taiga/base/api/pagination.py:115
|
||||||
msgid "Page is not 'last', nor can it be converted to an int."
|
msgid "Page is not 'last', nor can it be converted to an int."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:164
|
#: taiga/base/api/pagination.py:119
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Invalid page (%(page_number)s): %(message)s"
|
msgid "Invalid page (%(page_number)s): %(message)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:192
|
|
||||||
msgid ""
|
|
||||||
"The `filter_backend` attribute and `FILTER_BACKEND` setting are due to be "
|
|
||||||
"deprecated in favor of a `filter_backends` attribute and "
|
|
||||||
"`DEFAULT_FILTER_BACKENDS` setting, that take a *list* of filter backend "
|
|
||||||
"classes."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:213
|
|
||||||
msgid ""
|
|
||||||
"The `queryset` parameter to `get_paginate_by()` is due to be deprecated."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:234
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"'%s' should either include a 'serializer_class' attribute, or use the "
|
|
||||||
"'model' attribute as a shortcut for automatically generating a serializer "
|
|
||||||
"class."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:260
|
|
||||||
#, python-format
|
|
||||||
msgid "'%s' must define 'queryset' or 'model'"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:288
|
|
||||||
msgid ""
|
|
||||||
"The `pk_url_kwarg` attribute is due to be deprecated. Use the `lookup_field` "
|
|
||||||
"attribute instead"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:291
|
|
||||||
msgid ""
|
|
||||||
"The `slug_url_kwarg` attribute is due to be deprecated. Use the "
|
|
||||||
"`lookup_field` attribute instead"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/generics.py:294
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Expected view %s to be called with a URL keyword argument named \"%s\". Fix "
|
|
||||||
"your URL conf, or set the `.lookup_field` attribute on the view correctly."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: taiga/base/api/permissions.py:61
|
#: taiga/base/api/permissions.py:61
|
||||||
msgid "Invalid permission definition."
|
msgid "Invalid permission definition."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/views.py:97
|
#: taiga/base/api/relations.py:221
|
||||||
msgid "Not found"
|
#, python-format
|
||||||
|
msgid "Invalid pk '%s' - object does not exist."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:222
|
||||||
|
#, python-format
|
||||||
|
msgid "Incorrect type. Expected pk value, received %s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:310
|
||||||
|
#, python-format
|
||||||
|
msgid "Object with %s=%s does not exist."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:346
|
||||||
|
msgid "Invalid hyperlink - No URL match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:347
|
||||||
|
msgid "Invalid hyperlink - Incorrect URL match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:348
|
||||||
|
msgid "Invalid hyperlink due to configuration error"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:349
|
||||||
|
msgid "Invalid hyperlink - object does not exist."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/relations.py:350
|
||||||
|
#, python-format
|
||||||
|
msgid "Incorrect type. Expected url string, received %s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:77
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{0} is not a Django model"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:226
|
||||||
|
msgid "instance should be a queryset or other iterable with many=True"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:229
|
||||||
|
msgid ""
|
||||||
|
"allow_add_remove should only be used for bulk updates, but you have not set "
|
||||||
|
"many=True"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:296
|
||||||
|
msgid "Invalid data"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:388
|
||||||
|
msgid "No input provided"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:548
|
||||||
|
msgid "Cannot create a new item, only existing items may be updated."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/serializers.py:559
|
||||||
|
msgid "Expected a list of items."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/views.py:100
|
#: taiga/base/api/views.py:100
|
||||||
|
msgid "Not found"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/api/views.py:103
|
||||||
msgid "Permission denied"
|
msgid "Permission denied"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/views.py:349
|
#: taiga/base/api/views.py:352
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be "
|
"Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be "
|
||||||
"returned from the view, but received a `%s`"
|
"returned from the view, but received a `%s`"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/views.py:448
|
#: taiga/base/api/views.py:451
|
||||||
msgid "Server application error"
|
msgid "Server application error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/viewsets.py:57
|
#: taiga/base/api/viewsets.py:56
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You tried to pass in the %s method name as a keyword argument to %s(). Don't "
|
"You tried to pass in the %s method name as a keyword argument to %s(). Don't "
|
||||||
"do that."
|
"do that."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/api/viewsets.py:61
|
#: taiga/base/api/viewsets.py:60
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s() received an invalid keyword %r"
|
msgid "%s() received an invalid keyword %r"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -172,45 +295,84 @@ msgstr ""
|
||||||
msgid "Connection error."
|
msgid "Connection error."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:30
|
#: taiga/base/exceptions.py:53
|
||||||
|
msgid "Malformed request."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:58
|
||||||
|
msgid "Incorrect authentication credentials."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:63
|
||||||
|
msgid "Authentication credentials were not provided."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:68
|
||||||
|
msgid "You do not have permission to perform this action."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:73
|
||||||
|
#, python-format
|
||||||
|
msgid "Method '%s' not allowed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:81
|
||||||
|
msgid "Could not satisfy the request's Accept header"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:90
|
||||||
|
#, python-format
|
||||||
|
msgid "Unsupported media type '%s' in request."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:98
|
||||||
|
msgid "Request was throttled."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:99
|
||||||
|
#, python-format
|
||||||
|
msgid "Expected available in %d second%s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: taiga/base/exceptions.py:113
|
||||||
msgid "Unexpected error"
|
msgid "Unexpected error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:42
|
#: taiga/base/exceptions.py:125
|
||||||
msgid "Not found."
|
msgid "Not found."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:47
|
#: taiga/base/exceptions.py:130
|
||||||
msgid "Method not supported for this endpoint."
|
msgid "Method not supported for this endpoint."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:55 taiga/base/exceptions.py:63
|
#: taiga/base/exceptions.py:138 taiga/base/exceptions.py:146
|
||||||
msgid "Wrong arguments."
|
msgid "Wrong arguments."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:67
|
#: taiga/base/exceptions.py:150
|
||||||
msgid "Data validation error"
|
msgid "Data validation error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:79
|
#: taiga/base/exceptions.py:162
|
||||||
msgid "Integrity Error for wrong or invalid arguments"
|
msgid "Integrity Error for wrong or invalid arguments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/exceptions.py:86
|
#: taiga/base/exceptions.py:169
|
||||||
msgid "Precondition error"
|
msgid "Precondition error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/filters.py:63
|
#: taiga/base/filters.py:74
|
||||||
msgid "Error in filter params types."
|
msgid "Error in filter params types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/filters.py:107 taiga/base/filters.py:196
|
#: taiga/base/filters.py:118 taiga/base/filters.py:207
|
||||||
#: taiga/base/filters.py:253
|
#: taiga/base/filters.py:264
|
||||||
msgid "Filtering project diferent value than an integer: {}"
|
msgid "Filtering project diferent value than an integer: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/base/filters.py:110 taiga/base/filters.py:199
|
#: taiga/base/filters.py:121 taiga/base/filters.py:210
|
||||||
#: taiga/base/filters.py:255
|
#: taiga/base/filters.py:266
|
||||||
msgid "'project' must be an integer value."
|
msgid "'project' must be an integer value."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -356,21 +518,21 @@ msgstr ""
|
||||||
msgid "error importing user stories"
|
msgid "error importing user stories"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/export_import/serializers.py:157
|
#: taiga/export_import/serializers.py:158
|
||||||
msgid "{}=\"{}\" not found in this project"
|
msgid "{}=\"{}\" not found in this project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/export_import/serializers.py:378
|
#: taiga/export_import/serializers.py:379
|
||||||
#: taiga/projects/custom_attributes/serializers.py:104
|
#: taiga/projects/custom_attributes/serializers.py:103
|
||||||
msgid "Invalid content. It must be {\"key\": \"value\",...}"
|
msgid "Invalid content. It must be {\"key\": \"value\",...}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/export_import/serializers.py:393
|
#: taiga/export_import/serializers.py:394
|
||||||
#: taiga/projects/custom_attributes/serializers.py:119
|
#: taiga/projects/custom_attributes/serializers.py:118
|
||||||
msgid "It contain invalid custom fields."
|
msgid "It contain invalid custom fields."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/export_import/serializers.py:462
|
#: taiga/export_import/serializers.py:463
|
||||||
#: taiga/projects/milestones/serializers.py:63
|
#: taiga/projects/milestones/serializers.py:63
|
||||||
#: taiga/projects/serializers.py:64 taiga/projects/serializers.py:88
|
#: taiga/projects/serializers.py:64 taiga/projects/serializers.py:88
|
||||||
#: taiga/projects/serializers.py:110 taiga/projects/serializers.py:142
|
#: taiga/projects/serializers.py:110 taiga/projects/serializers.py:142
|
||||||
|
@ -822,7 +984,7 @@ msgstr ""
|
||||||
msgid "issue"
|
msgid "issue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/custom_attributes/serializers.py:58
|
#: taiga/projects/custom_attributes/serializers.py:57
|
||||||
msgid "Already exists one with the same name."
|
msgid "Already exists one with the same name."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -855,7 +1017,7 @@ msgstr ""
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:192
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:193
|
||||||
msgid "from"
|
msgid "from"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -864,7 +1026,7 @@ msgstr ""
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:198
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:199
|
||||||
msgid "to"
|
msgid "to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -902,7 +1064,7 @@ msgstr ""
|
||||||
msgid "Unassigned"
|
msgid "Unassigned"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:209
|
#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:211
|
||||||
#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86
|
#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86
|
||||||
msgid "-deleted-"
|
msgid "-deleted-"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1292,23 +1454,23 @@ msgid ""
|
||||||
"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n"
|
"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/notifications/validators.py:43
|
#: taiga/projects/notifications/validators.py:44
|
||||||
msgid "Watchers contains invalid users"
|
msgid "Watchers contains invalid users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/occ/mixins.py:34
|
#: taiga/projects/occ/mixins.py:35
|
||||||
msgid "The version must be an integer"
|
msgid "The version must be an integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/occ/mixins.py:55
|
#: taiga/projects/occ/mixins.py:56
|
||||||
msgid "The version is not valid"
|
msgid "The version is not valid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/occ/mixins.py:71
|
#: taiga/projects/occ/mixins.py:72
|
||||||
msgid "The version doesn't match with the current one"
|
msgid "The version doesn't match with the current one"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/occ/mixins.py:91
|
#: taiga/projects/occ/mixins.py:92
|
||||||
msgid "version"
|
msgid "version"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1492,7 +1654,7 @@ msgstr ""
|
||||||
msgid "generated from issue"
|
msgid "generated from issue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/projects/userstories/validators.py:12
|
#: taiga/projects/userstories/validators.py:28
|
||||||
msgid "There's no user story with that id"
|
msgid "There's no user story with that id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1671,11 +1833,11 @@ msgstr ""
|
||||||
msgid "permissions"
|
msgid "permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/users/serializers.py:53
|
#: taiga/users/serializers.py:51
|
||||||
msgid "invalid"
|
msgid "invalid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: taiga/users/serializers.py:64
|
#: taiga/users/serializers.py:62
|
||||||
msgid "Invalid username. Try with a different one."
|
msgid "Invalid username. Try with a different one."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,8 @@ import hashlib
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.serializers import ModelSerializer
|
|
||||||
from taiga.base.utils.urls import reverse
|
from taiga.base.utils.urls import reverse
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
|
@ -18,10 +18,9 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework.serializers import ValidationError
|
from taiga.base.fields import JsonField
|
||||||
|
from taiga.base.api.serializers import ValidationError
|
||||||
from taiga.base.serializers import ModelSerializer
|
from taiga.base.api.serializers import ModelSerializer
|
||||||
from taiga.base.serializers import JsonField
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.serializers import JsonField
|
|
||||||
|
from taiga.base.fields import JsonField
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import TagsField
|
||||||
|
from taiga.base.fields import PgArrayField
|
||||||
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin,
|
|
||||||
PgArrayField, ModelSerializer)
|
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
|
@ -26,7 +27,7 @@ from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class IssueSerializer(WatchersValidator, ModelSerializer):
|
class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
|
||||||
tags = TagsField(required=False)
|
tags = TagsField(required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
is_closed = serializers.Field(source="is_closed")
|
is_closed = serializers.Field(source="is_closed")
|
||||||
|
@ -63,13 +64,13 @@ class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
||||||
return NeighborIssueSerializer(neighbor).data
|
return NeighborIssueSerializer(neighbor).data
|
||||||
|
|
||||||
|
|
||||||
class NeighborIssueSerializer(ModelSerializer):
|
class NeighborIssueSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Issue
|
model = models.Issue
|
||||||
fields = ("id", "ref", "subject")
|
fields = ("id", "ref", "subject")
|
||||||
depth = 0
|
depth = 0
|
||||||
|
|
||||||
|
|
||||||
class IssuesBulkSerializer(ProjectExistsValidator, Serializer):
|
class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
bulk_issues = serializers.CharField()
|
bulk_issues = serializers.CharField()
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework import serializers
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
class WatchersValidator:
|
class WatchersValidator:
|
||||||
|
|
|
@ -21,6 +21,7 @@ from taiga.base import exceptions as exc
|
||||||
from taiga.base.utils import db
|
from taiga.base.utils import db
|
||||||
from taiga.projects.history.services import get_modified_fields
|
from taiga.projects.history.services import get_modified_fields
|
||||||
|
|
||||||
|
|
||||||
class OCCResourceMixin(object):
|
class OCCResourceMixin(object):
|
||||||
"""
|
"""
|
||||||
Rest Framework resource mixin for resources that need to have concurrent
|
Rest Framework resource mixin for resources that need to have concurrent
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
class ResolverSerializer(serializers.Serializer):
|
class ResolverSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -18,12 +18,12 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
from taiga.base.fields import JsonField
|
||||||
|
from taiga.base.fields import PgArrayField
|
||||||
|
from taiga.base.fields import TagsColorsField
|
||||||
|
|
||||||
from taiga.base.serializers import JsonField
|
|
||||||
from taiga.base.serializers import PgArrayField
|
|
||||||
from taiga.base.serializers import ModelSerializer
|
|
||||||
from taiga.base.serializers import TagsColorsField
|
|
||||||
from taiga.users.services import get_photo_or_gravatar_url
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
from taiga.users.serializers import UserSerializer
|
from taiga.users.serializers import UserSerializer
|
||||||
from taiga.users.serializers import ProjectRoleSerializer
|
from taiga.users.serializers import ProjectRoleSerializer
|
||||||
|
@ -44,7 +44,7 @@ from .custom_attributes.serializers import IssueCustomAttributeSerializer
|
||||||
## Custom values for selectors
|
## Custom values for selectors
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class PointsSerializer(ModelSerializer):
|
class PointsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Points
|
model = models.Points
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class PointsSerializer(ModelSerializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class UserStoryStatusSerializer(ModelSerializer):
|
class UserStoryStatusSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.UserStoryStatus
|
model = models.UserStoryStatus
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class UserStoryStatusSerializer(ModelSerializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusSerializer(ModelSerializer):
|
class TaskStatusSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.TaskStatus
|
model = models.TaskStatus
|
||||||
|
|
||||||
|
@ -112,17 +112,17 @@ class TaskStatusSerializer(ModelSerializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class SeveritySerializer(ModelSerializer):
|
class SeveritySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Severity
|
model = models.Severity
|
||||||
|
|
||||||
|
|
||||||
class PrioritySerializer(ModelSerializer):
|
class PrioritySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Priority
|
model = models.Priority
|
||||||
|
|
||||||
|
|
||||||
class IssueStatusSerializer(ModelSerializer):
|
class IssueStatusSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.IssueStatus
|
model = models.IssueStatus
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ class IssueStatusSerializer(ModelSerializer):
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class IssueTypeSerializer(ModelSerializer):
|
class IssueTypeSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.IssueType
|
model = models.IssueType
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ class IssueTypeSerializer(ModelSerializer):
|
||||||
## Members
|
## Members
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class MembershipSerializer(ModelSerializer):
|
class MembershipSerializer(serializers.ModelSerializer):
|
||||||
role_name = serializers.CharField(source='role.name', required=False, read_only=True)
|
role_name = serializers.CharField(source='role.name', required=False, read_only=True)
|
||||||
full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True)
|
full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True)
|
||||||
user_email = serializers.EmailField(source='user.email', required=False, read_only=True)
|
user_email = serializers.EmailField(source='user.email', required=False, read_only=True)
|
||||||
|
@ -238,7 +238,7 @@ class MembershipAdminSerializer(MembershipSerializer):
|
||||||
exclude = ("token",)
|
exclude = ("token",)
|
||||||
|
|
||||||
|
|
||||||
class ProjectMembershipSerializer(ModelSerializer):
|
class ProjectMembershipSerializer(serializers.ModelSerializer):
|
||||||
role_name = serializers.CharField(source='role.name', required=False)
|
role_name = serializers.CharField(source='role.name', required=False)
|
||||||
full_name = serializers.CharField(source='user.get_full_name', required=False)
|
full_name = serializers.CharField(source='user.get_full_name', required=False)
|
||||||
color = serializers.CharField(source='user.color', required=False)
|
color = serializers.CharField(source='user.color', required=False)
|
||||||
|
@ -266,7 +266,7 @@ class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer):
|
||||||
## Projects
|
## Projects
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectSerializer(ModelSerializer):
|
class ProjectSerializer(serializers.ModelSerializer):
|
||||||
tags = PgArrayField(required=False)
|
tags = PgArrayField(required=False)
|
||||||
anon_permissions = PgArrayField(required=False)
|
anon_permissions = PgArrayField(required=False)
|
||||||
public_permissions = PgArrayField(required=False)
|
public_permissions = PgArrayField(required=False)
|
||||||
|
@ -355,7 +355,7 @@ class ProjectDetailAdminSerializer(ProjectDetailSerializer):
|
||||||
## Starred
|
## Starred
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class StarredSerializer(ModelSerializer):
|
class StarredSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Project
|
model = models.Project
|
||||||
fields = ['id', 'name', 'slug']
|
fields = ['id', 'name', 'slug']
|
||||||
|
@ -366,7 +366,7 @@ class StarredSerializer(ModelSerializer):
|
||||||
## Project Templates
|
## Project Templates
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectTemplateSerializer(ModelSerializer):
|
class ProjectTemplateSerializer(serializers.ModelSerializer):
|
||||||
default_options = JsonField(required=False, label=_("Default options"))
|
default_options = JsonField(required=False, label=_("Default options"))
|
||||||
us_statuses = JsonField(required=False, label=_("User story's statuses"))
|
us_statuses = JsonField(required=False, label=_("User story's statuses"))
|
||||||
points = JsonField(required=False, label=_("Points"))
|
points = JsonField(required=False, label=_("Points"))
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin,
|
from taiga.base.fields import TagsField
|
||||||
PgArrayField, ModelSerializer)
|
from taiga.base.fields import PgArrayField
|
||||||
|
|
||||||
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
|
@ -28,7 +30,7 @@ from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class TaskSerializer(WatchersValidator, ModelSerializer):
|
class TaskSerializer(WatchersValidator, serializers.ModelSerializer):
|
||||||
tags = TagsField(required=False, default=[])
|
tags = TagsField(required=False, default=[])
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
comment = serializers.SerializerMethodField("get_comment")
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
@ -65,7 +67,7 @@ class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
||||||
return NeighborTaskSerializer(neighbor).data
|
return NeighborTaskSerializer(neighbor).data
|
||||||
|
|
||||||
|
|
||||||
class NeighborTaskSerializer(ModelSerializer):
|
class NeighborTaskSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Task
|
model = models.Task
|
||||||
fields = ("id", "ref", "subject")
|
fields = ("id", "ref", "subject")
|
||||||
|
@ -73,7 +75,7 @@ class NeighborTaskSerializer(ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
|
class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
|
||||||
TaskExistsValidator, Serializer):
|
TaskExistsValidator, serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
sprint_id = serializers.IntegerField()
|
sprint_id = serializers.IntegerField()
|
||||||
status_id = serializers.IntegerField(required=False)
|
status_id = serializers.IntegerField(required=False)
|
||||||
|
@ -82,11 +84,11 @@ class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
|
||||||
|
|
||||||
## Order bulk serializers
|
## Order bulk serializers
|
||||||
|
|
||||||
class _TaskOrderBulkSerializer(TaskExistsValidator, Serializer):
|
class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
|
||||||
task_id = serializers.IntegerField()
|
task_id = serializers.IntegerField()
|
||||||
order = serializers.IntegerField()
|
order = serializers.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, Serializer):
|
class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
bulk_tasks = _TaskOrderBulkSerializer(many=True)
|
bulk_tasks = _TaskOrderBulkSerializer(many=True)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -27,6 +26,7 @@ from django.http import HttpResponse
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
from taiga.base import status
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
|
@ -15,13 +15,10 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import TagsField
|
||||||
from taiga.base.serializers import Serializer
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.serializers import TagsField
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
from taiga.base.serializers import NeighborsSerializerMixin
|
|
||||||
from taiga.base.serializers import PgArrayField
|
|
||||||
from taiga.base.serializers import ModelSerializer
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
@ -43,7 +40,7 @@ class RolePointsField(serializers.WritableField):
|
||||||
return json.loads(obj)
|
return json.loads(obj)
|
||||||
|
|
||||||
|
|
||||||
class UserStorySerializer(WatchersValidator, ModelSerializer):
|
class UserStorySerializer(WatchersValidator, serializers.ModelSerializer):
|
||||||
tags = TagsField(default=[], required=False)
|
tags = TagsField(default=[], required=False)
|
||||||
external_reference = PgArrayField(required=False)
|
external_reference = PgArrayField(required=False)
|
||||||
points = RolePointsField(source="role_points", required=False)
|
points = RolePointsField(source="role_points", required=False)
|
||||||
|
@ -100,14 +97,14 @@ class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer
|
||||||
return NeighborUserStorySerializer(neighbor).data
|
return NeighborUserStorySerializer(neighbor).data
|
||||||
|
|
||||||
|
|
||||||
class NeighborUserStorySerializer(ModelSerializer):
|
class NeighborUserStorySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.UserStory
|
model = models.UserStory
|
||||||
fields = ("id", "ref", "subject")
|
fields = ("id", "ref", "subject")
|
||||||
depth = 0
|
depth = 0
|
||||||
|
|
||||||
|
|
||||||
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, Serializer):
|
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
status_id = serializers.IntegerField(required=False)
|
status_id = serializers.IntegerField(required=False)
|
||||||
bulk_stories = serializers.CharField()
|
bulk_stories = serializers.CharField()
|
||||||
|
@ -115,11 +112,11 @@ class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsVal
|
||||||
|
|
||||||
## Order bulk serializers
|
## Order bulk serializers
|
||||||
|
|
||||||
class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, Serializer):
|
class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
|
||||||
us_id = serializers.IntegerField()
|
us_id = serializers.IntegerField()
|
||||||
order = serializers.IntegerField()
|
order = serializers.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, Serializer):
|
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
bulk_stories = _UserStoryOrderBulkSerializer(many=True)
|
bulk_stories = _UserStoryOrderBulkSerializer(many=True)
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.users.models import User
|
from taiga.users.models import User
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from taiga.base.api.permissions import IsAuthenticated
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
from rest_framework import viewsets
|
from taiga.base.api import viewsets
|
||||||
|
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.serializers import JsonField
|
from taiga.base.fields import JsonField
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ from .contrib_routers import router as contrib_router
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^api/v1/', include(router.urls)),
|
url(r'^api/v1/', include(router.urls)),
|
||||||
url(r'^api/v1/', include(contrib_router.urls)),
|
url(r'^api/v1/', include(contrib_router.urls)),
|
||||||
url(r'^api/v1/api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url(r'^api/v1/api-auth/', include('taiga.base.api.urls', namespace='api')),
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,8 @@ from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.serializers import Serializer
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.serializers import ModelSerializer
|
|
||||||
from taiga.base.serializers import PgArrayField
|
|
||||||
|
|
||||||
from .models import User, Role
|
from .models import User, Role
|
||||||
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
||||||
|
@ -33,7 +31,7 @@ import re
|
||||||
## User
|
## User
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
full_name_display = serializers.SerializerMethodField("get_full_name_display")
|
full_name_display = serializers.SerializerMethodField("get_full_name_display")
|
||||||
photo = serializers.SerializerMethodField("get_photo")
|
photo = serializers.SerializerMethodField("get_photo")
|
||||||
big_photo = serializers.SerializerMethodField("get_big_photo")
|
big_photo = serializers.SerializerMethodField("get_big_photo")
|
||||||
|
@ -86,16 +84,16 @@ class UserAdminSerializer(UserSerializer):
|
||||||
read_only_fields = ("id", "email")
|
read_only_fields = ("id", "email")
|
||||||
|
|
||||||
|
|
||||||
class RecoverySerializer(Serializer):
|
class RecoverySerializer(serializers.Serializer):
|
||||||
token = serializers.CharField(max_length=200)
|
token = serializers.CharField(max_length=200)
|
||||||
password = serializers.CharField(min_length=6)
|
password = serializers.CharField(min_length=6)
|
||||||
|
|
||||||
|
|
||||||
class ChangeEmailSerializer(Serializer):
|
class ChangeEmailSerializer(serializers.Serializer):
|
||||||
email_token = serializers.CharField(max_length=200)
|
email_token = serializers.CharField(max_length=200)
|
||||||
|
|
||||||
|
|
||||||
class CancelAccountSerializer(Serializer):
|
class CancelAccountSerializer(serializers.Serializer):
|
||||||
cancel_token = serializers.CharField(max_length=200)
|
cancel_token = serializers.CharField(max_length=200)
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,7 +101,7 @@ class CancelAccountSerializer(Serializer):
|
||||||
## Role
|
## Role
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class RoleSerializer(ModelSerializer):
|
class RoleSerializer(serializers.ModelSerializer):
|
||||||
members_count = serializers.SerializerMethodField("get_members_count")
|
members_count = serializers.SerializerMethodField("get_members_count")
|
||||||
permissions = PgArrayField(required=False)
|
permissions = PgArrayField(required=False)
|
||||||
|
|
||||||
|
@ -115,7 +113,7 @@ class RoleSerializer(ModelSerializer):
|
||||||
return obj.memberships.count()
|
return obj.memberships.count()
|
||||||
|
|
||||||
|
|
||||||
class ProjectRoleSerializer(ModelSerializer):
|
class ProjectRoleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
fields = ('id', 'name', 'slug', 'order', 'computable')
|
fields = ('id', 'name', 'slug', 'order', 'computable')
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.serializers import JsonField
|
from taiga.base.fields import JsonField
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from rest_framework import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import TagsField, PgArrayField, JsonField
|
||||||
from taiga.base.serializers import TagsField, PgArrayField, JsonField
|
|
||||||
|
|
||||||
from taiga.projects.userstories import models as us_models
|
from taiga.projects.userstories import models as us_models
|
||||||
from taiga.projects.tasks import models as task_models
|
from taiga.projects.tasks import models as task_models
|
||||||
|
|
|
@ -19,8 +19,7 @@ import hashlib
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from rest_framework.renderers import UnicodeJSONRenderer
|
from taiga.base.api.renderers import UnicodeJSONRenderer
|
||||||
|
|
||||||
from taiga.base.utils.db import get_typename_for_model_instance
|
from taiga.base.utils.db import get_typename_for_model_instance
|
||||||
from taiga.celery import app
|
from taiga.celery import app
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue