taiga-back/taiga/base/api/settings.py

255 lines
8.3 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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/>.
# The code is partially taken (and modified) from django rest framework
# that is licensed under the following terms:
#
# Copyright (c) 2011-2014, Tom Christie
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or
# other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
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 six
import importlib
from . import ISO_8601
USER_SETTINGS = getattr(settings, "REST_FRAMEWORK", None)
DEFAULTS = {
# Base API policies
"DEFAULT_RENDERER_CLASSES": (
"taiga.base.api.renderers.JSONRenderer",
),
"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_MODEL_VALIDATOR_CLASS":
"taiga.base.api.validators.ModelValidator",
"DEFAULT_FILTER_BACKENDS": (),
# Throttling
"DEFAULT_THROTTLE_RATES": {
"user": None,
"anon": None,
},
"DEFAULT_THROTTLE_WHITELIST": [],
# 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": ISO_8601,
"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)