Merge pull request #290 from taigaio/us/1475/internationalization

i18n
remotes/origin/enhancement/email-actions
Alejandro 2015-04-15 10:52:42 +02:00
commit ace8e373c7
142 changed files with 14157 additions and 1222 deletions

16
.tx/config Normal file
View File

@ -0,0 +1,16 @@
[main]
host = https://www.transifex.com
lang_map = sr@latin:sr_Latn, zh_CN:zh_Hans, zh_TW:zh_Hant
[taiga-back.main]
file_filter = locale/<lang>/LC_MESSAGES/django.po
source_file = locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO
[taiga-back.taiga]
file_filter = taiga/locale/<lang>/LC_MESSAGES/django.po
source_file = taiga/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO

View File

@ -1,8 +1,17 @@
# Changelog #
## 1.6.0 Abies Bifolia (2015-03-17)
## 1.7.0 ??? (unreleased)
### Features
- Make Taiga translatable (i18n support).
### Misc
- Lots of small and not so small bugfixes.
- Remove djangorestframework from requirements. Move useful code to core.
## 1.6.0 Abies Bifolia (2015-03-17)
### Features
- Added custom fields per project for user stories, tasks and issues.

0
locale/empty Normal file
View File

View File

@ -9,3 +9,5 @@ pytest-pythonpath==0.6
coverage==3.7.1
coveralls==0.4.2
django-slowdown==0.0.1
transifex-client==0.11.1b0

View File

@ -1,5 +1,5 @@
djangorestframework==2.3.13
Django==1.7.6
#djangorestframework==2.3.13 # It's not necessary since Taiga 1.7
django-picklefield==0.3.1
django-sampledatahelper==0.2.2
gunicorn==19.1.1

View File

@ -0,0 +1,242 @@
#!/usr/bin/env python
#
# NOTE: This script is based on django's manage_translations.py script
# (https://github.com/django/django/blob/master/scripts/manage_translations.py)
#
# This python file contains utility scripts to manage taiga translations.
# It has to be run inside the taiga-back git root directory.
#
# The following commands are available:
#
# * update_catalogs: check for new strings in taiga-back catalogs, and
# output how much strings are new/changed.
#
# * lang_stats: output statistics for each catalog/language combination
#
# * fetch: fetch translations from transifex.com
#
# Each command support the --languages and --resources options to limit their
# operation to the specified language or resource. For example, to get stats
# for Spanish in contrib.admin, run:
#
# $ python scripts/manage_translations.py lang_stats --language=es --resources=taiga
import os
from argparse import ArgumentParser
from argparse import RawTextHelpFormatter
from subprocess import PIPE, Popen, call
from django_jinja.management.commands import makemessages
def _get_locale_dirs(resources):
"""
Return a tuple (app name, absolute path) for all locale directories.
If resources list is not None, filter directories matching resources content.
"""
contrib_dir = os.getcwd()
dirs = []
# Collect all locale directories
for contrib_name in os.listdir(contrib_dir):
path = os.path.join(contrib_dir, contrib_name, "locale")
if os.path.isdir(path):
dirs.append((contrib_name, path))
# Filter by resources, if any
if resources is not None:
res_names = [d[0] for d in dirs]
dirs = [ld for ld in dirs if ld[0] in resources]
if len(resources) > len(dirs):
print("You have specified some unknown resources. "
"Available resource names are: {0}".format(", ".join(res_names)))
exit(1)
return dirs
def _tx_resource_for_name(name):
""" Return the Transifex resource name """
return "taiga-back.{}".format(name)
def _check_diff(cat_name, base_path):
"""
Output the approximate number of changed/added strings in the en catalog.
"""
po_path = "{path}/en/LC_MESSAGES/django.po".format(path=base_path)
p = Popen("git diff -U0 {0} | egrep '^[-+]msgid' | wc -l".format(po_path),
stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
num_changes = int(output.strip())
print("{0} changed/added messages in '{1}' catalog.".format(num_changes, cat_name))
def update_catalogs(resources=None, languages=None):
"""
Update the en/LC_MESSAGES/django.po (all) files with
new/updated translatable strings.
"""
cmd = makemessages.Command()
opts = {
"locale": ["en"],
"extensions": ["py", "jinja"],
# Default values
"domain": "django",
"all": False,
"symlinks": False,
"ignore_patterns": [],
"use_default_ignore_patterns": True,
"no_wrap": False,
"no_location": False,
"no_obsolete": False,
"keep_pot": False,
"verbosity": "0",
}
if resources is not None:
print("`update_catalogs` will always process all resources.")
os.chdir(os.getcwd())
print("Updating en catalogs for all taiga-back resourcess...")
cmd.handle(**opts)
# Output changed stats
contrib_dirs = _get_locale_dirs(None)
for name, dir_ in contrib_dirs:
_check_diff(name, dir_)
def lang_stats(resources=None, languages=None):
"""
Output language statistics of committed translation files for each catalog.
If resources is provided, it should be a list of translation resource to
limit the output (e.g. ['main', 'taiga']).
"""
locale_dirs = _get_locale_dirs(resources)
for name, dir_ in locale_dirs:
print("\nShowing translations stats for '{res}':".format(res=name))
langs = sorted([d for d in os.listdir(dir_) if not d.startswith('_')])
for lang in langs:
if languages and lang not in languages:
continue
# TODO: merge first with the latest en catalog
p = Popen("msgfmt -vc -o /dev/null {path}/{lang}/LC_MESSAGES/django.po".format(path=dir_, lang=lang),
stdout=PIPE, stderr=PIPE, shell=True)
output, errors = p.communicate()
if p.returncode == 0:
# msgfmt output stats on stderr
print("{0}: {1}".format(lang, errors.strip().decode("utf-8")))
else:
print("Errors happened when checking {0} translation for {1}:\n{2}".format(lang, name, errors))
def fetch(resources=None, languages=None):
"""
Fetch translations from Transifex, wrap long lines, generate mo files.
"""
locale_dirs = _get_locale_dirs(resources)
errors = []
for name, dir_ in locale_dirs:
# Transifex pull
if languages is None:
call("tx pull -r {res} -a -f --minimum-perc=5".format(res=_tx_resource_for_name(name)), shell=True)
languages = sorted([d for d in os.listdir(dir_) if not d.startswith("_") and d != "en"])
else:
for lang in languages:
call("tx pull -r {res} -f -l {lang}".format(res=_tx_resource_for_name(name), lang=lang), shell=True)
# msgcat to wrap lines and msgfmt for compilation of .mo file
for lang in languages:
po_path = "{path}/{lang}/LC_MESSAGES/django.po".format(path=dir_, lang=lang)
if not os.path.exists(po_path):
print("No {lang} translation for resource {res}".format(lang=lang, res=name))
continue
call("msgcat -o {0} {0}".format(po_path), shell=True)
res = call("msgfmt -c -o {0}.mo {1}".format(po_path[:-3], po_path), shell=True)
if res != 0:
errors.append((name, lang))
if errors:
print("\nWARNING: Errors have occurred in following cases:")
for resource, lang in errors:
print("\tResource {res} for language {lang}".format(res=resource, lang=lang))
exit(1)
def commit(resources=None, languages=None):
"""
Commit messages to Transifex,
"""
locale_dirs = _get_locale_dirs(resources)
errors = []
for name, dir_ in locale_dirs:
# Transifex push
if languages is None:
call("tx push -r {res} -s -l en".format(res=_tx_resource_for_name(name)), shell=True)
else:
for lang in languages:
call("tx push -r {res} -l {lang}".format(res= _tx_resource_for_name(name), lang=lang), shell=True)
if __name__ == "__main__":
try:
devnull = open(os.devnull)
Popen(["tx"], stdout=devnull, stderr=devnull).communicate()
except OSError as e:
if e.errno == os.errno.ENOENT:
print("""
You need transifex-client, install it.
1. Install transifex-client, use
$ pip install --upgrade -r requirements-devel.txt
or
$ pip install --upgrade transifex-client==0.11.1b0
2. Create ~/.transifexrc file:
$ vim ~/.transifexrc"
[https://www.transifex.com]
hostname = https://www.transifex.com
username = <YOUR_USERNAME>
password = <YOUR_PASSWOR>
""")
exit(1)
RUNABLE_SCRIPTS = {
"update_catalogs": "regenerate .po files of main lang (en).",
"commit": "send .po file to transifex ('en' by default).",
"fetch": "get .po files from transifex and regenerate .mo files.",
"lang_stats": "get stats of local translations",
}
parser = ArgumentParser(description="manage translations in taiga-back between the repo and transifex.",
formatter_class=RawTextHelpFormatter)
parser.add_argument("cmd", nargs=1,
help="\n".join(["{0} - {1}".format(c, h) for c, h in RUNABLE_SCRIPTS.items()]))
parser.add_argument("-r", "--resources", action="append",
help="limit operation to the specified resources")
parser.add_argument("-l", "--languages", action="append",
help="limit operation to the specified languages")
options = parser.parse_args()
if options.cmd[0] in RUNABLE_SCRIPTS.keys():
eval(options.cmd[0])(options.resources, options.languages)
else:
print("Available commands are: {}".format(", ".join(RUNABLE_SCRIPTS.keys())))

View File

@ -15,7 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os.path, sys, os
from django.utils.translation import ugettext_lazy as _
# This is defined here as a do-nothing function because we can't import
# django.utils.translation -- that module depends on the settings.
gettext_noop = lambda s: s
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@ -26,11 +30,6 @@ ADMINS = (
("Admin", "example@example.com"),
)
LANGUAGES = (
("en", _("English")),
("es", _("Spanish")),
)
DATABASES = {
"default": {
"ENGINE": "transaction_hooks.backends.postgresql_psycopg2",
@ -60,12 +59,108 @@ IGNORABLE_404_STARTS = ("/phpmyadmin/",)
ATOMIC_REQUESTS = True
TIME_ZONE = "UTC"
LANGUAGE_CODE = "en"
USE_I18N = True
USE_L10N = True
LOGIN_URL="/auth/login/"
USE_TZ = True
USE_I18N = True
USE_L10N = True
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
# Languages we provide translations for, out of the box.
LANGUAGES = [
("af", "Afrikaans"), # Afrikaans
("ar", "العربية‏"), # Arabic
("ast", "Asturiano"), # Asturian
("az", "Azərbaycan dili"), # Azerbaijani
("bg", "Български"), # Bulgarian
("be", "Беларуская"), # Belarusian
("bn", "বাংলা"), # Bengali
("br", "Bretón"), # Breton
("bs", "Bosanski"), # Bosnian
("ca", "Català"), # Catalan
("cs", "Čeština"), # Czech
("cy", "Cymraeg"), # Welsh
("da", "Dansk"), # Danish
("de", "Deutsch"), # German
("el", "Ελληνικά"), # Greek
("en", "English (US)"), # English
("en-au", "English (Australia)"), # Australian English
("en-gb", "English (UK)"), # British English
("eo", "esperanta"), # Esperanto
("es", "Español"), # Spanish
("es-ar", "Español (Argentina)"), # Argentinian Spanish
("es-mx", "Español (México)"), # Mexican Spanish
("es-ni", "Español (Nicaragua)"), # Nicaraguan Spanish
("es-ve", "Español (Venezuela)"), # Venezuelan Spanish
("et", "Eesti"), # Estonian
("eu", "Euskara"), # Basque
("fa", "فارسی‏"), # Persian
("fi", "Suomi"), # Finnish
("fr", "Français"), # French
("fy", "Frysk"), # Frisian
("ga", "Irish"), # Irish
("gl", "Galego"), # Galician
("he", "עברית‏"), # Hebrew
("hi", "हिन्दी"), # Hindi
("hr", "Hrvatski"), # Croatian
("hu", "Magyar"), # Hungarian
("ia", "Interlingua"), # Interlingua
("id", "Bahasa Indonesia"), # Indonesian
("io", "IDO"), # Ido
("is", "Íslenska"), # Icelandic
("it", "Italiano"), # Italian
("ja", "日本語"), # Japanese
("ka", "ქართული"), # Georgian
("kk", "Қазақша"), # Kazakh
("km", "ភាសាខ្មែរ"), # Khmer
("kn", "ಕನ್ನಡ"), # Kannada
("ko", "한국어"), # Korean
("lb", "Lëtzebuergesch"), # Luxembourgish
("lt", "Lietuvių"), # Lithuanian
("lv", "Latviešu"), # Latvian
("mk", "Македонски"), # Macedonian
("ml", "മലയാളം"), # Malayalam
("mn", "Монгол"), # Mongolian
("mr", "मराठी"), # Marathi
("my", "မြန်မာ"), # Burmese
("nb", "Norsk (bokmål)"), # Norwegian Bokmal
("ne", "नेपाली"), # Nepali
("nl", "Nederlands"), # Dutch
("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk
("os", "Ирон æвзаг"), # Ossetic
("pa", "ਪੰਜਾਬੀ"), # Punjabi
("pl", "Polski"), # Polish
("pt", "Português (Portugal)"), # Portuguese
("pt-br", "Português (Brasil)"), # Brazilian Portuguese
("ro", "Română"), # Romanian
("ru", "Русский"), # Russian
("sk", "Slovenčina"), # Slovak
("sl", "Slovenščina"), # Slovenian
("sq", "Shqip"), # Albanian
("sr", "Српски"), # Serbian
("sr-latn", "srpski"), # Serbian Latin
("sv", "Svenska"), # Swedish
("sw", "Kiswahili"), # Swahili
("ta", "தமிழ்"), # Tamil
("te", "తెలుగు"), # Telugu
("th", "ภาษาไทย"), # Thai
("tr", "Türkçe"), # Turkish
("tt", "татар теле"), # Tatar
("udm", "удмурт кыл"), # Udmurt
("uk", "Українська"), # Ukrainian
("ur", "اردو‏"), # Urdu
("vi", "Tiếng Việt"), # Vietnamese
("zh-hans", "中文(简体)"), # Simplified Chinese
("zh-hant", "中文(香港)"), # Traditional Chinese
]
# Languages using BiDi (right-to-left) layout
LANGUAGES_BIDI = ["he", "ar", "fa", "ur"]
SITES = {
"api": {"domain": "localhost:8000", "scheme": "http", "name": "api"},
"front": {"domain": "localhost:9001", "scheme": "http", "name": "front"},
@ -126,6 +221,7 @@ DEFAULT_FILE_STORAGE = "taiga.base.storage.FileSystemStorage"
LOCALE_PATHS = (
os.path.join(BASE_DIR, "locale"),
os.path.join(BASE_DIR, "taiga", "locale"),
)
SECRET_KEY = "aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e"
@ -174,6 +270,8 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"taiga.base",
"taiga.base.api",
"taiga.locale",
"taiga.events",
"taiga.front",
"taiga.users",
@ -200,7 +298,6 @@ INSTALLED_APPS = [
"taiga.hooks.bitbucket",
"taiga.webhooks",
"rest_framework",
"djmail",
"django_jinja",
"django_jinja.contrib._humanize",

View File

@ -17,11 +17,10 @@
from functools import partial
from enum import Enum
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
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.decorators import list_route
from taiga.base import exceptions as exc

View File

@ -35,7 +35,7 @@ fraudulent modifications.
import re
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
@ -43,7 +43,7 @@ from .tokens import get_user_for_token
class Session(BaseAuthentication):
"""
Session based authentication like the standard
`rest_framework.authentication.SessionAuthentication`
`taiga.base.api.authentication.SessionAuthentication`
but with csrf disabled (for obvious reasons because
it is for api.

View File

@ -14,10 +14,12 @@
# 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 rest_framework import serializers
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
import re
@ -29,13 +31,13 @@ class BaseRegisterSerializer(serializers.Serializer):
def validate_username(self, attrs, source):
value = attrs[source]
validator = validators.RegexValidator(re.compile('^[\w.-]+$'), "invalid username", "invalid")
validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid")
try:
validator(value)
except ValidationError:
raise serializers.ValidationError("Required. 255 characters or fewer. Letters, numbers "
"and /./-/_ characters'")
raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
"and /./-/_ characters'"))
return attrs

View File

@ -58,7 +58,7 @@ def send_register_email(user) -> bool:
cancel_token = get_token_for_user(user, "cancel_account")
context = {"user": user, "cancel_token": cancel_token}
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
email = mbuilder.registered_user(user.email, context)
email = mbuilder.registered_user(user, context)
return bool(email.send())
@ -91,7 +91,7 @@ def get_membership_by_token(token:str):
membership_model = apps.get_model("projects", "Membership")
qs = membership_model.objects.filter(token=token)
if len(qs) == 0:
raise exc.NotFound("Token not matches any valid invitation.")
raise exc.NotFound(_("Token not matches any valid invitation."))
return qs[0]
@ -119,7 +119,7 @@ def public_register(username:str, password:str, email:str, full_name:str):
try:
user.save()
except IntegrityError:
raise exc.WrongArguments("User is already register.")
raise exc.WrongArguments(_("User is already registered."))
send_register_email(user)
user_registered_signal.send(sender=user.__class__, user=user)
@ -143,7 +143,7 @@ def private_register_for_existing_user(token:str, username:str, password:str):
membership.user = user
membership.save(update_fields=["user"])
except IntegrityError:
raise exc.IntegrityError("Membership with user is already exists.")
raise exc.IntegrityError(_("Membership with user is already exists."))
send_register_email(user)
return user

View File

@ -18,6 +18,7 @@ from taiga.base import exceptions as exc
from django.apps import apps
from django.core import signing
from django.utils.translation import ugettext as _
def get_token_for_user(user, scope):
@ -43,13 +44,13 @@ def get_user_for_token(token, scope, max_age=None):
try:
data = signing.loads(token, max_age=max_age)
except signing.BadSignature:
raise exc.NotAuthenticated("Invalid token")
raise exc.NotAuthenticated(_("Invalid token"))
model_cls = apps.get_model("users", "User")
try:
user = model_cls.objects.get(pk=data["user_%s_id" % (scope)])
except (model_cls.DoesNotExist, KeyError):
raise exc.NotAuthenticated("Invalid token")
raise exc.NotAuthenticated(_("Invalid token"))
else:
return user

View File

@ -17,6 +17,15 @@
# This code is partially taken from django-rest-framework:
# 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 ModelCrudViewSet
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)

1059
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:
# Copyright (c) 2011-2014, Tom Christie
import warnings
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
from django.utils.translation import ugettext as _
from rest_framework.settings import api_settings
from . import views
from . import mixins
from . import pagination
from .settings import api_settings
from .utils import get_object_or_404
def strict_positive_int(integer_string, cutoff=None):
"""
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):
class GenericAPIView(pagination.PaginationMixin,
views.APIView):
"""
Base class for all other generic views.
"""
@ -63,20 +48,12 @@ class GenericAPIView(views.APIView):
lookup_field = 'pk'
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
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
# The following attributes may be subject to change,
# and should be considered private API.
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
paginator_class = Paginator
######################################
# These are pending deprecation...
@ -107,70 +84,6 @@ class GenericAPIView(views.APIView):
return serializer_class(instance, data=data, files=files,
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):
"""
@ -202,29 +115,6 @@ class GenericAPIView(views.APIView):
# 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):
if self.action == "list" and hasattr(self, "list_serializer_class"):
return self.list_serializer_class
@ -233,11 +123,9 @@ class GenericAPIView(views.APIView):
if serializer_class is not None:
return serializer_class
assert self.model is not None, \
"'%s' should either include a 'serializer_class' attribute, " \
"or use the 'model' attribute as a shortcut for " \
"automatically generating a serializer class." \
% self.__class__.__name__
assert self.model is not None, ("'%s' should either include a 'serializer_class' attribute, "
"or use the 'model' attribute as a shortcut for "
"automatically generating a serializer class." % self.__class__.__name__)
class DefaultSerializer(self.model_serializer_class):
class Meta:
@ -261,7 +149,7 @@ class GenericAPIView(views.APIView):
if self.model is not None:
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):
"""
@ -289,18 +177,16 @@ class GenericAPIView(views.APIView):
if lookup is not None:
filter_kwargs = {self.lookup_field: lookup}
elif pk is not None and self.lookup_field == 'pk':
raise RuntimeError('The `pk_url_kwarg` attribute is due to be deprecated. '
'Use the `lookup_field` attribute instead')
raise RuntimeError(('The `pk_url_kwarg` attribute is due to be deprecated. '
'Use the `lookup_field` attribute instead'))
elif slug is not None and self.lookup_field == 'pk':
raise RuntimeError('The `slug_url_kwarg` attribute is due to be deprecated. '
'Use the `lookup_field` attribute instead')
raise RuntimeError(('The `slug_url_kwarg` attribute is due to be deprecated. '
'Use the `lookup_field` attribute instead'))
else:
raise ImproperlyConfigured(
'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.' %
(self.__class__.__name__, self.lookup_field)
)
raise ImproperlyConfigured(('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.' %
(self.__class__.__name__, self.lookup_field)))
obj = get_object_or_404(queryset, **filter_kwargs)
return obj

View File

@ -22,10 +22,11 @@ import warnings
from django.core.exceptions import ValidationError
from django.http import Http404
from django.db import transaction as tx
from django.utils.translation import ugettext as _
from taiga.base import response
from rest_framework.settings import api_settings
from .settings import api_settings
from .utils import get_object_or_404
@ -94,12 +95,10 @@ class ListModelMixin(object):
# Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets.
if not self.allow_empty and not self.object_list:
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
)
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)
class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)

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
# 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 get_paginate_by(self, *args, **kwargs):
def strict_positive_int(integer_string, cutoff=None):
"""
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:
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):
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:
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

View File

@ -20,6 +20,7 @@ from taiga.base.utils import sequence as sq
from taiga.permissions.service import user_has_perm, is_project_owner
from django.apps import apps
from django.utils.translation import ugettext as _
######################################################################
# Base permissiones definition
@ -57,7 +58,7 @@ class ResourcePermission(object):
elif inspect.isclass(permset) and issubclass(permset, PermissionComponent):
permset = permset()
else:
raise RuntimeError("Invalid permission definition.")
raise RuntimeError(_("Invalid permission definition."))
if self.global_perms:
permset = (self.global_perms & permset)

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,25 +19,29 @@
import json
from django.conf import settings
from django.core.exceptions import PermissionDenied
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.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 rest_framework import status, exceptions
from rest_framework.compat import smart_text, HttpResponseBase, View
from rest_framework.request import Request
from rest_framework.settings import api_settings
from rest_framework.utils import formatting
from .request import Request
from .settings import api_settings
from .utils import formatting
from taiga.base import status
from taiga.base import exceptions
from taiga.base.response import Response
from taiga.base.response import Ok
from taiga.base.response import NotFound
from taiga.base.response import Forbidden
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):
@ -93,10 +97,10 @@ def exception_handler(exc):
headers=headers)
elif isinstance(exc, Http404):
return NotFound({'detail': 'Not found'})
return NotFound({'detail': _('Not found')})
elif isinstance(exc, PermissionDenied):
return Forbidden({'detail': 'Permission denied'})
return Forbidden({'detail': _('Permission denied')})
# Note: Unhandled exceptions will raise a 500 error.
return None
@ -292,7 +296,7 @@ class APIView(View):
"""
request.user
def check_permissions(self, request, action, obj=None):
def check_permissions(self, request, action:str=None, obj=None):
if action is None:
self.permission_denied(request)
@ -345,11 +349,9 @@ class APIView(View):
Returns the final response object.
"""
# Make the error obvious if a proper response is not returned
assert isinstance(response, HttpResponseBase), (
'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
'to be returned from the view, but received a `%s`'
% type(response)
)
assert isinstance(response, HttpResponseBase), _('Expected a `Response`, `HttpResponse` or '
'`HttpStreamingResponse` to be returned from the view, '
'but received a `%s`' % type(response))
if isinstance(response, Response):
if not getattr(request, 'accepted_renderer', None):
@ -446,6 +448,6 @@ class APIView(View):
def api_server_error(request, *args, **kwargs):
if settings.DEBUG is False and request.META['CONTENT_TYPE'] == "application/json":
return HttpResponse(json.dumps({"error": "Server application error"}),
return HttpResponse(json.dumps({"error": _("Server application error")}),
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return server_error(request, *args, **kwargs)

View File

@ -19,11 +19,11 @@
from functools import update_wrapper
from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext as _
from . import views
from . import mixins
from . import generics
from . import pagination
class ViewSetMixin(object):
@ -53,12 +53,12 @@ class ViewSetMixin(object):
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
raise TypeError(_("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__)))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r" % (
cls.__name__, key))
raise TypeError(_("%s() received an invalid keyword %r"
% (cls.__name__, key)))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
@ -125,9 +125,7 @@ class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
pass
class ReadOnlyListViewSet(pagination.HeadersPaginationMixin,
pagination.ConditionalPaginationMixin,
GenericViewSet):
class ReadOnlyListViewSet(GenericViewSet):
"""
A viewset that provides default `list()` action.
"""
@ -156,15 +154,11 @@ class ModelViewSet(mixins.CreateModelMixin,
pass
class ModelCrudViewSet(pagination.HeadersPaginationMixin,
pagination.ConditionalPaginationMixin,
ModelViewSet):
class ModelCrudViewSet(ModelViewSet):
pass
class ModelListViewSet(pagination.HeadersPaginationMixin,
pagination.ConditionalPaginationMixin,
mixins.RetrieveModelMixin,
class ModelListViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet):
pass

View File

@ -13,19 +13,10 @@
#
# 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/>.
import sys
# Patch api view for correctly return 401 responses on
# request is authenticated instead of 403
from django.apps import AppConfig
from . import monkey
class BaseAppConfig(AppConfig):
name = "taiga.base"
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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from rest_framework import exceptions
from rest_framework import status
# This code is partially taken from django-rest-framework:
# 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.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
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
default_detail = _("Unexpected error")
@ -67,7 +150,7 @@ class RequestValidationError(BadRequest):
default_detail = _("Data validation error")
class PermissionDenied(exceptions.PermissionDenied):
class PermissionDenied(PermissionDenied):
"""
Compatibility subclass of restframework `PermissionDenied`
exception.
@ -86,7 +169,7 @@ class PreconditionError(BadRequest):
default_detail = _("Precondition error")
class NotAuthenticated(exceptions.NotAuthenticated):
class NotAuthenticated(NotAuthenticated):
"""
Compatibility subclass of restframework `NotAuthenticated`
exception.
@ -119,7 +202,7 @@ def exception_handler(exc):
to be raised.
"""
if isinstance(exc, exceptions.APIException):
if isinstance(exc, APIException):
headers = {}
if getattr(exc, "auth_header", None):
headers["WWW-Authenticate"] = exc.auth_header

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

@ -0,0 +1,109 @@
# 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 django.utils.translation import ugettext as _
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 I18NJsonField(JsonField):
"""
Json objects serializer.
"""
widget = widgets.Textarea
def __init__(self, i18n_fields=(), *args, **kwargs):
super(I18NJsonField, self).__init__(*args, **kwargs)
self.i18n_fields = i18n_fields
def translate_values(self, d):
i18n_d = {}
for key, value in d.items():
if isinstance(value, dict):
i18n_d[key] = self.translate_values(value)
if key in self.i18n_fields:
if isinstance(value, list):
i18n_d[key] = [_(e) for e in value]
if isinstance(value, str):
i18n_d[key] = _(value)
else:
i18n_d[key] = value
return i18n_d
def to_native(self, obj):
i18n_obj = self.translate_values(obj)
return i18n_obj
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

@ -19,9 +19,7 @@ import logging
from django.apps import apps
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from rest_framework import filters
from django.utils.translation import ugettext as _
from taiga.base import exceptions as exc
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__)
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 = {
'true': True,
'false': False,
@ -60,7 +71,7 @@ class QueryParamsFilterMixin(filters.BaseFilterBackend):
try:
queryset = queryset.filter(**query_params)
except ValueError:
raise exc.BadRequest("Error in filter params types.")
raise exc.BadRequest(_("Error in filter params types."))
return queryset
@ -104,10 +115,10 @@ class PermissionBasedFilterBackend(FilterBackend):
try:
project_id = int(request.QUERY_PARAMS["project"])
except:
logger.error("Filtering project diferent value than an integer: {}".format(
logger.error(_("Filtering project diferent value than an integer: {}".format(
request.QUERY_PARAMS["project"]
))
raise exc.BadRequest("'project' must be an integer value.")
)))
raise exc.BadRequest(_("'project' must be an integer value."))
qs = queryset
@ -193,10 +204,10 @@ class CanViewProjectObjFilterBackend(FilterBackend):
try:
project_id = int(request.QUERY_PARAMS["project"])
except:
logger.error("Filtering project diferent value than an integer: {}".format(
logger.error(_("Filtering project diferent value than an integer: {}".format(
request.QUERY_PARAMS["project"]
))
raise exc.BadRequest("'project' must be an integer value.")
)))
raise exc.BadRequest(_("'project' must be an integer value."))
qs = queryset
@ -250,8 +261,9 @@ class MembersFilterBackend(PermissionBasedFilterBackend):
try:
project_id = int(request.QUERY_PARAMS["project"])
except:
logger.error("Filtering project diferent value than an integer: {}".format(request.QUERY_PARAMS["project"]))
raise exc.BadRequest("'project' must be an integer value.")
logger.error(_("Filtering project diferent value than an integer: {}".format(
request.QUERY_PARAMS["project"])))
raise exc.BadRequest(_("'project' must be an integer value."))
if project_id:
Project = apps.get_model('projects', 'Project')

View File

@ -16,6 +16,8 @@
import datetime
from optparse import make_option
from django.db.models.loading import get_model
from django.core.management.base import BaseCommand
from django.utils import timezone
@ -30,6 +32,11 @@ from taiga.users.models import User
class Command(BaseCommand):
args = '<email>'
option_list = BaseCommand.option_list + (
make_option('--locale', '-l', default=None, dest='locale',
help='Send emails in an specific language.'),
)
help = 'Send an example of all emails'
def handle(self, *args, **options):
@ -37,12 +44,13 @@ class Command(BaseCommand):
print("Usage: ./manage.py test_emails <email-address>")
return
locale = options.get('locale')
test_email = args[0]
mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail)
# Register email
context = {"user": User.objects.all().order_by("?").first(), "cancel_token": "cancel-token"}
context = {"lang": locale, "user": User.objects.all().order_by("?").first(), "cancel_token": "cancel-token"}
email = mbuilder.registered_user(test_email, context)
email.send()
@ -51,17 +59,18 @@ class Command(BaseCommand):
membership.invited_by = User.objects.all().order_by("?").first()
membership.invitation_extra_text = "Text example, Text example,\nText example,\n\nText example"
context = {"membership": membership}
context = {"lang": locale, "membership": membership}
email = mbuilder.membership_invitation(test_email, context)
email.send()
# Membership notification
context = {"membership": Membership.objects.order_by("?").filter(user__isnull=False).first()}
context = {"lang": locale, "membership": Membership.objects.order_by("?").filter(user__isnull=False).first()}
email = mbuilder.membership_notification(test_email, context)
email.send()
# Feedback
context = {
"lang": locale,
"feedback_entry": {
"full_name": "Test full name",
"email": "test@email.com",
@ -76,17 +85,18 @@ class Command(BaseCommand):
email.send()
# Password recovery
context = {"user": User.objects.all().order_by("?").first()}
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
email = mbuilder.password_recovery(test_email, context)
email.send()
# Change email
context = {"user": User.objects.all().order_by("?").first()}
context = {"lang": locale, "user": User.objects.all().order_by("?").first()}
email = mbuilder.change_email(test_email, context)
email.send()
# Export/Import emails
context = {
"lang": locale,
"user": User.objects.all().order_by("?").first(),
"project": Project.objects.all().order_by("?").first(),
"error_subject": "Error generating project dump",
@ -95,6 +105,7 @@ class Command(BaseCommand):
email = mbuilder.export_error(test_email, context)
email.send()
context = {
"lang": locale,
"user": User.objects.all().order_by("?").first(),
"error_subject": "Error importing project dump",
"error_message": "Error importing project dump",
@ -104,6 +115,7 @@ class Command(BaseCommand):
deletion_date = timezone.now() + datetime.timedelta(seconds=60*60*24)
context = {
"lang": locale,
"url": "http://dummyurl.com",
"user": User.objects.all().order_by("?").first(),
"project": Project.objects.all().order_by("?").first(),
@ -113,6 +125,7 @@ class Command(BaseCommand):
email.send()
context = {
"lang": locale,
"user": User.objects.all().order_by("?").first(),
"project": Project.objects.all().order_by("?").first(),
}
@ -139,6 +152,7 @@ class Command(BaseCommand):
]
context = {
"lang": locale,
"project": Project.objects.all().order_by("?").first(),
"changer": User.objects.all().order_by("?").first(),
"history_entries": HistoryEntry.objects.all().order_by("?")[0:5],

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 taiga.base.api import serializers
Neighbor = namedtuple("Neighbor", "left right")
@ -67,3 +69,25 @@ def get_neighbors(obj, results_set=None):
right = None
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
# 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."""
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):
def __init__(self, data=None, status=None, template_name=None, headers=None, exception=False,
content_type=None):
super(Response, self).__init__(data, status, template_name, headers, exception,
content_type)
class Response(SimpleTemplateResponse):
"""
An HttpResponse that allows its data to be rendered into
arbitrary media types.
"""
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):

View File

@ -22,10 +22,10 @@ from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
from taiga.base.api import views
from taiga.base import response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
from taiga.base.api.reverse import reverse
from taiga.base.api.urlpatterns import format_suffix_patterns
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
# 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):

View File

@ -14,10 +14,12 @@
# 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/>.
import json
from rest_framework.utils import encoders
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):
return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii)

View File

@ -15,6 +15,8 @@
# 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.utils.translation import ugettext as _
from contextlib import contextmanager
@ -22,7 +24,7 @@ from contextlib import contextmanager
def without_signals(*disablers):
for disabler in disablers:
if not (isinstance(disabler, list) or isinstance(disabler, tuple)) or len(disabler) == 0:
raise ValueError("The parameters must be lists of at least one parameter (the signal)")
raise ValueError(_("The parameters must be lists of at least one parameter (the signal)."))
signal, *ids = disabler
signal.backup_receivers = signal.receivers

View File

@ -19,7 +19,7 @@ import codecs
import uuid
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from django.db.transaction import atomic
from django.db.models import signals
from django.conf import settings

View File

@ -14,6 +14,8 @@
# 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.utils.translation import ugettext as _
from taiga.projects.models import Membership
from . import serializers
@ -83,7 +85,7 @@ def dict_to_project(data, owner=None):
project_serialized = service.store_project(data)
if not project_serialized:
raise TaigaImportError('error importing project')
raise TaigaImportError(_('error importing project'))
proj = project_serialized.object
@ -96,12 +98,12 @@ def dict_to_project(data, owner=None):
service.store_choices(proj, data, "severities", serializers.SeverityExportSerializer)
if service.get_errors(clear=False):
raise TaigaImportError('error importing choices')
raise TaigaImportError(_('error importing choices'))
service.store_default_choices(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing default choices')
raise TaigaImportError(_('error importing default choices'))
service.store_custom_attributes(proj, data, "userstorycustomattributes",
serializers.UserStoryCustomAttributeExportSerializer)
@ -111,12 +113,12 @@ def dict_to_project(data, owner=None):
serializers.IssueCustomAttributeExportSerializer)
if service.get_errors(clear=False):
raise TaigaImportError('error importing custom attributes')
raise TaigaImportError(_('error importing custom fields'))
service.store_roles(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing roles')
raise TaigaImportError(_('error importing roles'))
service.store_memberships(proj, data)
@ -131,37 +133,37 @@ def dict_to_project(data, owner=None):
)
if service.get_errors(clear=False):
raise TaigaImportError('error importing memberships')
raise TaigaImportError(_('error importing memberships'))
store_milestones(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing milestones')
raise TaigaImportError(_('error importing milestones'))
store_wiki_pages(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing wiki pages')
raise TaigaImportError(_('error importing wiki pages'))
store_wiki_links(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing wiki links')
raise TaigaImportError(_('error importing wiki links'))
store_issues(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing issues')
raise TaigaImportError(_('error importing issues'))
store_user_stories(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing user stories')
raise TaigaImportError(_('error importing user stories'))
store_tasks(proj, data)
if service.get_errors(clear=False):
raise TaigaImportError('error importing issues')
raise TaigaImportError(_('error importing issues'))
store_tags_colors(proj, data)

View File

@ -14,7 +14,7 @@
# 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 rest_framework.renderers import UnicodeJSONRenderer
from taiga.base.api.renderers import UnicodeJSONRenderer
class ExportRenderer(UnicodeJSONRenderer):

View File

@ -18,13 +18,17 @@ import base64
import os
from collections import OrderedDict
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext as _
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.custom_attributes import models as custom_attributes_models
@ -38,8 +42,6 @@ from taiga.projects.attachments import models as attachments_models
from taiga.users import models as users_models
from taiga.projects.votes import services as votes_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):
@ -153,7 +155,7 @@ class ProjectRelatedField(serializers.RelatedField):
kwargs = {self.slug_field: data, "project": self.context['project']}
return self.queryset.get(**kwargs)
except ObjectDoesNotExist:
raise ValidationError("{}=\"{}\" not found in this project".format(self.slug_field, data))
raise ValidationError(_("{}=\"{}\" not found in this project".format(self.slug_field, data)))
class HistoryUserField(JsonField):
@ -458,7 +460,7 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
name = attrs[source]
qs = self.project.milestones.filter(name=name)
if qs.exists():
raise serializers.ValidationError("Name duplicated for the project")
raise serializers.ValidationError(_("Name duplicated for the project"))
return attrs

View File

@ -20,6 +20,7 @@ from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.utils import timezone
from django.conf import settings
from django.utils.translation import ugettext as _
from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail
@ -45,11 +46,11 @@ def dump_project(self, user, project):
except Exception:
ctx = {
"user": user,
"error_subject": "Error generating project dump",
"error_message": "Error generating project dump",
"error_subject": _("Error generating project dump"),
"error_message": _("Error generating project dump"),
"project": project
}
email = mbuilder.export_error(user.email, ctx)
email = mbuilder.export_error(user, ctx)
email.send()
return
@ -60,7 +61,7 @@ def dump_project(self, user, project):
"user": user,
"deletion_date": deletion_date
}
email = mbuilder.dump_project(user.email, ctx)
email = mbuilder.dump_project(user, ctx)
email.send()
@ -78,13 +79,13 @@ def load_project_dump(user, dump):
except Exception:
ctx = {
"user": user,
"error_subject": "Error loading project dump",
"error_message": "Error loading project dump",
"error_subject": _("Error loading project dump"),
"error_message": _("Error loading project dump"),
}
email = mbuilder.import_error(user.email, ctx)
email = mbuilder.import_error(user, ctx)
email.send()
return
ctx = {"user": user, "project": project}
email = mbuilder.load_dump(user.email, ctx)
email = mbuilder.load_dump(user, ctx)
email.send()

View File

@ -14,7 +14,7 @@
# 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 rest_framework import serializers
from taiga.base.api import serializers
from . import models

View File

@ -14,7 +14,7 @@
# 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.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from taiga.base import exceptions as exc
from taiga.base import response

View File

@ -16,7 +16,7 @@
import re
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from taiga.base import exceptions as exc
from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus
@ -27,11 +27,10 @@ from taiga.projects.history.services import take_snapshot
from taiga.projects.notifications.services import send_notifications
from taiga.hooks.event_hooks import BaseEventHook
from taiga.hooks.exceptions import ActionSyntaxException
from taiga.base.utils import json
from .services import get_bitbucket_user
import json
class PushEventHook(BaseEventHook):
def process_event(self):
@ -92,7 +91,7 @@ class PushEventHook(BaseEventHook):
element.save()
snapshot = take_snapshot(element,
comment="Status changed from BitBucket commit",
comment=_("Status changed from BitBucket commit"),
user=get_bitbucket_user(bitbucket_user))
send_notifications(element, history=snapshot)

View File

@ -14,7 +14,7 @@
# 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.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus
@ -85,7 +85,7 @@ class PushEventHook(BaseEventHook):
element.save()
snapshot = take_snapshot(element,
comment="Status changed from GitHub commit",
comment=_("Status changed from GitHub commit"),
user=get_github_user(github_user))
send_notifications(element, history=snapshot)
@ -93,7 +93,7 @@ class PushEventHook(BaseEventHook):
def replace_github_references(project_url, wiki_text):
if wiki_text == None:
wiki_text = ""
template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url)
return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M)
@ -125,7 +125,7 @@ class IssuesEventHook(BaseEventHook):
)
take_snapshot(issue, user=get_github_user(github_user))
snapshot = take_snapshot(issue, comment="Created from GitHub", user=get_github_user(github_user))
snapshot = take_snapshot(issue, comment=_("Created from GitHub"), user=get_github_user(github_user))
send_notifications(issue, history=snapshot)
@ -149,6 +149,6 @@ class IssueCommentEventHook(BaseEventHook):
for item in list(issues) + list(tasks) + list(uss):
snapshot = take_snapshot(item,
comment="From GitHub:\n\n{}".format(comment_message),
comment=_("From GitHub:\n\n{}".format(comment_message)),
user=get_github_user(github_user))
send_notifications(item, history=snapshot)

View File

@ -17,7 +17,7 @@
import re
import os
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus
@ -84,7 +84,7 @@ class PushEventHook(BaseEventHook):
element.save()
snapshot = take_snapshot(element,
comment="Status changed from GitLab commit",
comment=_("Status changed from GitLab commit"),
user=get_gitlab_user(gitlab_user))
send_notifications(element, history=snapshot)
@ -126,5 +126,5 @@ class IssuesEventHook(BaseEventHook):
)
take_snapshot(issue, user=get_gitlab_user(None))
snapshot = take_snapshot(issue, comment="Created from GitLab", user=get_gitlab_user(None))
snapshot = take_snapshot(issue, comment=_("Created from GitLab"), user=get_gitlab_user(None))
send_notifications(issue, history=snapshot)

0
taiga/locale/__init__.py Normal file
View File

30
taiga/locale/api.py Normal file
View File

@ -0,0 +1,30 @@
# 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/>.
from django.conf import settings
from taiga.base import response
from taiga.base.api.viewsets import ReadOnlyListViewSet
from . import permissions
class LocalesViewSet(ReadOnlyListViewSet):
permission_classes = (permissions.LocalesPermission,)
def list(self, request, *args, **kwargs):
locales = [{"code": c, "name": n} for c, n in settings.LANGUAGES]
return response.Ok(locales)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
# 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/>.
from taiga.base.api.permissions import TaigaResourcePermission, AllowAny
class LocalesPermission(TaigaResourcePermission):
global_perms = AllowAny()

View File

@ -18,7 +18,7 @@ import uuid
from django.db.models import signals
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from taiga.base import filters
from taiga.base import response
@ -186,10 +186,10 @@ class ProjectViewSet(ModelCrudViewSet):
template_description = request.DATA.get('template_description', None)
if not template_name:
raise response.BadRequest("Not valid template name")
raise response.BadRequest(_("Not valid template name"))
if not template_description:
raise response.BadRequest("Not valid template description")
raise response.BadRequest(_("Not valid template description"))
template_slug = slugify_uniquely(template_name, models.ProjectTemplate)

View File

@ -14,24 +14,18 @@
# 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/>.
import os
import os.path as path
import hashlib
import mimetypes
mimetypes.init()
from django.utils.translation import ugettext as _
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django import http
from taiga.base import filters
from taiga.base import exceptions as exc
from taiga.base.api import generics
from taiga.base.api import ModelCrudViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.users.models import User
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.history.mixins import HistoryResourceMixin
@ -50,7 +44,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru
def update(self, *args, **kwargs):
partial = kwargs.get("partial", False)
if not partial:
raise exc.NotSupported("Non partial updates not supported")
raise exc.NotSupported(_("Non partial updates not supported"))
return super().update(*args, **kwargs)
def get_content_type(self):
@ -65,7 +59,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCru
obj.name = path.basename(obj.attached_file.name).lower()
if obj.project_id != obj.content_object.project_id:
raise exc.WrongArguments("Project ID not matches between object and project")
raise exc.WrongArguments(_("Project ID not matches between object and project"))
super().pre_save(obj)

View File

@ -19,9 +19,8 @@ import hashlib
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 . import models

View File

@ -14,7 +14,10 @@
# 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.utils.translation import ugettext_lazy as _
VIDEOCONFERENCES_CHOICES = (
("appear-in", "AppearIn"),
("talky", "Talky"),
("appear-in", _("AppearIn")),
("talky", _("Talky")),
)

View File

@ -78,7 +78,7 @@ class IssueCustomAttribute(AbstractCustomAttribute):
#######################################################
class AbstractCustomAttributesValues(OCCModelMixin, models.Model):
attributes_values = JsonField(null=False, blank=False, default={}, verbose_name=_("attributes_values"))
attributes_values = JsonField(null=False, blank=False, default={}, verbose_name=_("values"))
class Meta:
abstract = True

View File

@ -18,10 +18,9 @@
from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from rest_framework.serializers import ValidationError
from taiga.base.serializers import ModelSerializer
from taiga.base.serializers import JsonField
from taiga.base.fields import JsonField
from taiga.base.api.serializers import ValidationError
from taiga.base.api.serializers import ModelSerializer
from . import models

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
from django.utils import timezone
from taiga.base import response
@ -66,7 +67,7 @@ class HistoryViewSet(ReadOnlyListViewSet):
return response.NotFound()
if comment.delete_comment_date or comment.delete_comment_user:
return response.BadRequest({"error": "Comment already deleted"})
return response.BadRequest({"error": _("Comment already deleted")})
comment.delete_comment_date = timezone.now()
comment.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()}
@ -85,7 +86,7 @@ class HistoryViewSet(ReadOnlyListViewSet):
return response.NotFound()
if not comment.delete_comment_date and not comment.delete_comment_user:
return response.BadRequest({"error": "Comment not deleted"})
return response.BadRequest({"error": _("Comment not deleted")})
comment.delete_comment_date = None
comment.delete_comment_user = None

View File

@ -33,7 +33,7 @@ class HistoryResourceMixin(object):
def get_last_history(self):
if not self.__object_saved:
message = ("get_last_history() function called before any object are saved. "
message = ("get_last_history() function called before any object are saved. "
"Seems you have a wrong mixing order on your resource.")
warnings.warn(message, RuntimeWarning)
return self.__last_history

View File

@ -14,20 +14,20 @@
# 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 rest_framework import serializers
from taiga.base.serializers import JsonField
from taiga.base.api import serializers
from taiga.base.fields import JsonField, I18NJsonField
from . import models
HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type")
class HistoryEntrySerializer(serializers.ModelSerializer):
diff = JsonField()
snapshot = JsonField()
values = JsonField()
values_diff = JsonField()
values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
user = JsonField()
delete_comment_user = JsonField()
class Meta:
model = models.HistoryEntry

View File

@ -14,7 +14,7 @@
# 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.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from django.db.models import Q
from django.http import Http404, HttpResponse

View File

@ -14,10 +14,11 @@
# 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 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.projects.validators import ProjectExistsValidator
@ -26,7 +27,7 @@ from taiga.projects.notifications.validators import WatchersValidator
from . import models
class IssueSerializer(WatchersValidator, ModelSerializer):
class IssueSerializer(WatchersValidator, serializers.ModelSerializer):
tags = TagsField(required=False)
external_reference = PgArrayField(required=False)
is_closed = serializers.Field(source="is_closed")
@ -63,13 +64,13 @@ class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
return NeighborIssueSerializer(neighbor).data
class NeighborIssueSerializer(ModelSerializer):
class NeighborIssueSerializer(serializers.ModelSerializer):
class Meta:
model = models.Issue
fields = ("id", "ref", "subject")
depth = 0
class IssuesBulkSerializer(ProjectExistsValidator, Serializer):
class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
bulk_issues = serializers.CharField()

View File

@ -72,7 +72,7 @@ class Milestone(WatchedModelMixin, models.Model):
def clean(self):
# Don't allow draft entries to have a pub_date.
if self.estimated_start and self.estimated_finish and self.estimated_start > self.estimated_finish:
raise ValidationError('The estimated start must be previous to the estimated finish.')
raise ValidationError(_('The estimated start must be previous to the estimated finish.'))
def save(self, *args, **kwargs):
if not self._importing or not self.modified_date:

View File

@ -14,15 +14,16 @@
# 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/>.
import json
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 ..userstories.serializers import UserStorySerializer
from . import models
class MilestoneSerializer(serializers.ModelSerializer):
user_stories = UserStorySerializer(many=True, required=False, read_only=True)
total_points = serializers.SerializerMethodField("get_total_points")
@ -59,6 +60,6 @@ class MilestoneSerializer(serializers.ModelSerializer):
qs = models.Milestone.objects.filter(project=attrs["project"], name=attrs[source])
if qs and qs.exists():
raise serializers.ValidationError("Name duplicated for the project")
raise serializers.ValidationError(_("Name duplicated for the project"))
return attrs

View File

@ -1,6 +1,6 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _
from rest_framework import serializers
from taiga.base.api import serializers
from . import models

Some files were not shown because too many files have changed in this diff Show More