Add code of django-restframwork to taiga

remotes/origin/enhancement/email-actions
David Barragán Merino 2015-04-07 23:34:44 +02:00
parent d3fade9565
commit 910d71eefc
92 changed files with 8186 additions and 587 deletions

View File

@ -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"

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

1048
taiga/base/api/fields.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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,9 +123,9 @@ 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__)
class DefaultSerializer(self.model_serializer_class): class DefaultSerializer(self.model_serializer_class):
class Meta: class Meta:
@ -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,16 +177,16 @@ 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)))
obj = get_object_or_404(queryset, **filter_kwargs) obj = get_object_or_404(queryset, **filter_kwargs)
return obj return obj

View File

@ -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

View File

@ -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(",")]

View File

@ -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

220
taiga/base/api/parsers.py Normal file
View File

@ -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

628
taiga/base/api/relations.py Normal file
View File

@ -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()

613
taiga/base/api/renderers.py Normal file
View File

@ -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)

440
taiga/base/api/request.py Normal file
View File

@ -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)

41
taiga/base/api/reverse.py Normal file
View File

@ -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

226
taiga/base/api/settings.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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"}})();

View File

@ -0,0 +1,3 @@
{% extends "api/base.html" %}
{# Override this template in your own templates directory to customize #}

View File

@ -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">&rsaquo;</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>

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
{% extends "api/login_base.html" %}
{# Override this template in your own templates directory to customize #}

View File

@ -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>

View File

@ -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 %}

View File

View File

@ -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 = [("(", ")"), ("<", ">"), ("[", "]"), ("&lt;", "&gt;"),
("\"", "\""), ("'", "'")]
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

View File

@ -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
}

View File

@ -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)

43
taiga/base/api/urls.py Normal file
View File

@ -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"),
)

View File

@ -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

View File

@ -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, [])

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

77
taiga/base/fields.py Normal file
View File

@ -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())

View File

@ -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,

View File

@ -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

View File

@ -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)
}

View File

@ -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):

View File

@ -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'])

View File

@ -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

89
taiga/base/status.py Normal file
View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

@ -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"))

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)),
] ]

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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