Merge pull request #290 from taigaio/us/1475/internationalization
i18nremotes/origin/enhancement/email-actions
commit
ace8e373c7
|
@ -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
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())))
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Provides various authentication policies.
|
||||
"""
|
||||
import base64
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
|
||||
from taiga.base import exceptions
|
||||
|
||||
from . import HTTP_HEADER_ENCODING
|
||||
|
||||
|
||||
def get_authorization_header(request):
|
||||
"""
|
||||
Return request's 'Authorization:' header, as a bytestring.
|
||||
|
||||
Hide some test client ickyness where the header can be unicode.
|
||||
"""
|
||||
auth = request.META.get('HTTP_AUTHORIZATION', b'')
|
||||
if type(auth) == type(''):
|
||||
# Work around django test client oddness
|
||||
auth = auth.encode(HTTP_HEADER_ENCODING)
|
||||
return auth
|
||||
|
||||
|
||||
class CSRFCheck(CsrfViewMiddleware):
|
||||
def _reject(self, request, reason):
|
||||
# Return the failure reason instead of an HttpResponse
|
||||
return reason
|
||||
|
||||
|
||||
class BaseAuthentication(object):
|
||||
"""
|
||||
All authentication classes should extend BaseAuthentication.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Authenticate the request and return a two-tuple of (user, token).
|
||||
"""
|
||||
raise NotImplementedError(".authenticate() must be overridden.")
|
||||
|
||||
def authenticate_header(self, request):
|
||||
"""
|
||||
Return a string to be used as the value of the `WWW-Authenticate`
|
||||
header in a `401 Unauthenticated` response, or `None` if the
|
||||
authentication scheme should return `403 Permission Denied` responses.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BasicAuthentication(BaseAuthentication):
|
||||
"""
|
||||
HTTP Basic authentication against username/password.
|
||||
"""
|
||||
www_authenticate_realm = 'api'
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Returns a `User` if a correct username and password have been supplied
|
||||
using HTTP Basic authentication. Otherwise returns `None`.
|
||||
"""
|
||||
auth = get_authorization_header(request).split()
|
||||
|
||||
if not auth or auth[0].lower() != b'basic':
|
||||
return None
|
||||
|
||||
if len(auth) == 1:
|
||||
msg = 'Invalid basic header. No credentials provided.'
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
elif len(auth) > 2:
|
||||
msg = 'Invalid basic header. Credentials string should not contain spaces.'
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
try:
|
||||
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
|
||||
except (TypeError, UnicodeDecodeError):
|
||||
msg = 'Invalid basic header. Credentials not correctly base64 encoded'
|
||||
raise exceptions.AuthenticationFailed(msg)
|
||||
|
||||
userid, password = auth_parts[0], auth_parts[2]
|
||||
return self.authenticate_credentials(userid, password)
|
||||
|
||||
def authenticate_credentials(self, userid, password):
|
||||
"""
|
||||
Authenticate the userid and password against username and password.
|
||||
"""
|
||||
user = authenticate(username=userid, password=password)
|
||||
if user is None or not user.is_active:
|
||||
raise exceptions.AuthenticationFailed('Invalid username/password')
|
||||
return (user, None)
|
||||
|
||||
def authenticate_header(self, request):
|
||||
return 'Basic realm="%s"' % self.www_authenticate_realm
|
||||
|
||||
|
||||
class SessionAuthentication(BaseAuthentication):
|
||||
"""
|
||||
Use Django's session framework for authentication.
|
||||
"""
|
||||
|
||||
def authenticate(self, request):
|
||||
"""
|
||||
Returns a `User` if the request session currently has a logged in user.
|
||||
Otherwise returns `None`.
|
||||
"""
|
||||
|
||||
# Get the underlying HttpRequest object
|
||||
request = request._request
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
# Unauthenticated, CSRF validation not required
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
|
||||
self.enforce_csrf(request)
|
||||
|
||||
# CSRF passed with authenticated user
|
||||
return (user, None)
|
||||
|
||||
def enforce_csrf(self, request):
|
||||
"""
|
||||
Enforce CSRF validation for session based authentication.
|
||||
"""
|
||||
reason = CSRFCheck().process_view(request, None, (), {})
|
||||
if reason:
|
||||
# CSRF failed, bail with explicit error message
|
||||
raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
|
File diff suppressed because it is too large
Load Diff
|
@ -17,33 +17,18 @@
|
|||
# This code is partially taken from django-rest-framework:
|
||||
# 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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Content negotiation deals with selecting an appropriate renderer given the
|
||||
incoming request. Typically this will be based on the request's Accept header.
|
||||
"""
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from taiga.base import exceptions
|
||||
from .settings import api_settings
|
||||
|
||||
from .utils.mediatypes import order_by_precedence
|
||||
from .utils.mediatypes import media_type_matches
|
||||
from .utils.mediatypes import _MediaType
|
||||
|
||||
|
||||
class BaseContentNegotiation(object):
|
||||
def select_parser(self, request, parsers):
|
||||
raise NotImplementedError(".select_parser() must be implemented")
|
||||
|
||||
def select_renderer(self, request, renderers, format_suffix=None):
|
||||
raise NotImplementedError(".select_renderer() must be implemented")
|
||||
|
||||
|
||||
class DefaultContentNegotiation(BaseContentNegotiation):
|
||||
settings = api_settings
|
||||
|
||||
def select_parser(self, request, parsers):
|
||||
"""
|
||||
Given a list of parsers and a media type, return the appropriate
|
||||
parser to handle the incoming request.
|
||||
"""
|
||||
for parser in parsers:
|
||||
if media_type_matches(parser.media_type, request.content_type):
|
||||
return parser
|
||||
return None
|
||||
|
||||
def select_renderer(self, request, renderers, format_suffix=None):
|
||||
"""
|
||||
Given a request and a list of renderers, return a two-tuple of:
|
||||
(renderer, media type).
|
||||
"""
|
||||
# Allow URL style format override. eg. "?format=json
|
||||
format_query_param = self.settings.URL_FORMAT_OVERRIDE
|
||||
format = format_suffix or request.QUERY_PARAMS.get(format_query_param)
|
||||
|
||||
if format:
|
||||
renderers = self.filter_renderers(renderers, format)
|
||||
|
||||
accepts = self.get_accept_list(request)
|
||||
|
||||
# Check the acceptable media types against each renderer,
|
||||
# attempting more specific media types first
|
||||
# NB. The inner loop here isni't as bad as it first looks :)
|
||||
# Worst case is we"re looping over len(accept_list) * len(self.renderers)
|
||||
for media_type_set in order_by_precedence(accepts):
|
||||
for renderer in renderers:
|
||||
for media_type in media_type_set:
|
||||
if media_type_matches(renderer.media_type, media_type):
|
||||
# Return the most specific media type as accepted.
|
||||
if (_MediaType(renderer.media_type).precedence >
|
||||
_MediaType(media_type).precedence):
|
||||
# Eg client requests "*/*"
|
||||
# Accepted media type is "application/json"
|
||||
return renderer, renderer.media_type
|
||||
else:
|
||||
# Eg client requests "application/json; indent=8"
|
||||
# Accepted media type is "application/json; indent=8"
|
||||
return renderer, media_type
|
||||
|
||||
raise exceptions.NotAcceptable(available_renderers=renderers)
|
||||
|
||||
def filter_renderers(self, renderers, format):
|
||||
"""
|
||||
If there is a ".json" style format suffix, filter the renderers
|
||||
so that we only negotiation against those that accept that format.
|
||||
"""
|
||||
renderers = [renderer for renderer in renderers
|
||||
if renderer.format == format]
|
||||
if not renderers:
|
||||
raise Http404
|
||||
return renderers
|
||||
|
||||
def get_accept_list(self, request):
|
||||
"""
|
||||
Given the incoming request, return a tokenised list of media
|
||||
type strings.
|
||||
|
||||
Allows URL style accept override. eg. "?accept=application/json"
|
||||
"""
|
||||
header = request.META.get("HTTP_ACCEPT", "*/*")
|
||||
header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header)
|
||||
return [token.strip() for token in header.split(",")]
|
|
@ -14,19 +14,112 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# 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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,628 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Serializer fields that deal with relationships.
|
||||
|
||||
These fields allow you to specify the style that should be used to represent
|
||||
model relationships, including hyperlinks, primary keys, or slugs.
|
||||
"""
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .fields import Field, WritableField, get_component, is_simple_callable
|
||||
from .reverse import reverse
|
||||
|
||||
import warnings
|
||||
from urllib import parse as urlparse
|
||||
|
||||
|
||||
|
||||
|
||||
##### Relational fields #####
|
||||
|
||||
|
||||
# Not actually Writable, but subclasses may need to be.
|
||||
class RelatedField(WritableField):
|
||||
"""
|
||||
Base class for related model fields.
|
||||
|
||||
This represents a relationship using the unicode representation of the target.
|
||||
"""
|
||||
widget = widgets.Select
|
||||
many_widget = widgets.SelectMultiple
|
||||
form_field_class = forms.ChoiceField
|
||||
many_form_field_class = forms.MultipleChoiceField
|
||||
null_values = (None, "", "None")
|
||||
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
read_only = True
|
||||
many = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# "null" is to be deprecated in favor of "required"
|
||||
if "null" in kwargs:
|
||||
warnings.warn("The `null` keyword argument is deprecated. "
|
||||
"Use the `required` keyword argument instead.",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
kwargs["required"] = not kwargs.pop("null")
|
||||
|
||||
queryset = kwargs.pop("queryset", None)
|
||||
self.many = kwargs.pop("many", self.many)
|
||||
if self.many:
|
||||
self.widget = self.many_widget
|
||||
self.form_field_class = self.many_form_field_class
|
||||
|
||||
kwargs["read_only"] = kwargs.pop("read_only", self.read_only)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
if not self.required:
|
||||
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||
|
||||
self.queryset = queryset
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
super(RelatedField, self).initialize(parent, field_name)
|
||||
if self.queryset is None and not self.read_only:
|
||||
manager = getattr(self.parent.opts.model, self.source or field_name)
|
||||
if hasattr(manager, "related"): # Forward
|
||||
self.queryset = manager.related.model._default_manager.all()
|
||||
else: # Reverse
|
||||
self.queryset = manager.field.rel.to._default_manager.all()
|
||||
|
||||
### We need this stuff to make form choices work...
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_text(obj)
|
||||
ident = smart_text(self.to_native(obj))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def _get_queryset(self):
|
||||
return self._queryset
|
||||
|
||||
def _set_queryset(self, queryset):
|
||||
self._queryset = queryset
|
||||
self.widget.choices = self.choices
|
||||
|
||||
queryset = property(_get_queryset, _set_queryset)
|
||||
|
||||
def _get_choices(self):
|
||||
# If self._choices is set, then somebody must have manually set
|
||||
# the property self.choices. In this case, just return self._choices.
|
||||
if hasattr(self, "_choices"):
|
||||
return self._choices
|
||||
|
||||
# Otherwise, execute the QuerySet in self.queryset to determine the
|
||||
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
|
||||
# consumed. Note that we"re instantiating a new ModelChoiceIterator *each*
|
||||
# time _get_choices() is called (and, thus, each time self.choices is
|
||||
# accessed) so that we can ensure the QuerySet has not been consumed. This
|
||||
# construct might look complicated but it allows for lazy evaluation of
|
||||
# the queryset.
|
||||
return ModelChoiceIterator(self)
|
||||
|
||||
def _set_choices(self, value):
|
||||
# Setting choices also sets the choices on the widget.
|
||||
# choices can be any iterable, but we call list() on it because
|
||||
# it will be consumed more than once.
|
||||
self._choices = self.widget.choices = list(value)
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
### Default value handling
|
||||
|
||||
def get_default_value(self):
|
||||
default = super(RelatedField, self).get_default_value()
|
||||
if self.many and default is None:
|
||||
return []
|
||||
return default
|
||||
|
||||
### Regular serializer stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
if self.source == "*":
|
||||
return self.to_native(obj)
|
||||
|
||||
source = self.source or field_name
|
||||
value = obj
|
||||
|
||||
for component in source.split("."):
|
||||
if value is None:
|
||||
break
|
||||
value = get_component(value, component)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if self.many:
|
||||
if is_simple_callable(getattr(value, "all", None)):
|
||||
return [self.to_native(item) for item in value.all()]
|
||||
else:
|
||||
# Also support non-queryset iterables.
|
||||
# This allows us to also support plain lists of related items.
|
||||
return [self.to_native(item) for item in value]
|
||||
return self.to_native(value)
|
||||
|
||||
def field_from_native(self, data, files, field_name, into):
|
||||
if self.read_only:
|
||||
return
|
||||
|
||||
try:
|
||||
if self.many:
|
||||
try:
|
||||
# Form data
|
||||
value = data.getlist(field_name)
|
||||
if value == [""] or value == []:
|
||||
raise KeyError
|
||||
except AttributeError:
|
||||
# Non-form data
|
||||
value = data[field_name]
|
||||
else:
|
||||
value = data[field_name]
|
||||
except KeyError:
|
||||
if self.partial:
|
||||
return
|
||||
value = self.get_default_value()
|
||||
|
||||
if value in self.null_values:
|
||||
if self.required:
|
||||
raise ValidationError(self.error_messages["required"])
|
||||
into[(self.source or field_name)] = None
|
||||
elif self.many:
|
||||
into[(self.source or field_name)] = [self.from_native(item) for item in value]
|
||||
else:
|
||||
into[(self.source or field_name)] = self.from_native(value)
|
||||
|
||||
|
||||
### PrimaryKey relationships
|
||||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a relationship as a pk value.
|
||||
"""
|
||||
read_only = False
|
||||
|
||||
default_error_messages = {
|
||||
"does_not_exist": _("Invalid pk '%s' - object does not exist."),
|
||||
"incorrect_type": _("Incorrect type. Expected pk value, received %s."),
|
||||
}
|
||||
|
||||
# TODO: Remove these field hacks...
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_text(obj)
|
||||
ident = smart_text(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
# TODO: Possibly change this to just take `obj`, through prob less performant
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception("Writable related fields must include a `queryset` argument")
|
||||
|
||||
try:
|
||||
return self.queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
msg = self.error_messages["does_not_exist"] % smart_text(data)
|
||||
raise ValidationError(msg)
|
||||
except (TypeError, ValueError):
|
||||
received = type(data).__name__
|
||||
msg = self.error_messages["incorrect_type"] % received
|
||||
raise ValidationError(msg)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
if self.many:
|
||||
# To-many relationship
|
||||
|
||||
queryset = None
|
||||
if not self.source:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
try:
|
||||
queryset = obj.serializable_value(field_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
if queryset is None:
|
||||
# RelatedManager (reverse relationship)
|
||||
source = self.source or field_name
|
||||
queryset = obj
|
||||
for component in source.split("."):
|
||||
if queryset is None:
|
||||
return []
|
||||
queryset = get_component(queryset, component)
|
||||
|
||||
# Forward relationship
|
||||
if is_simple_callable(getattr(queryset, "all", None)):
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
else:
|
||||
# Also support non-queryset iterables.
|
||||
# This allows us to also support plain lists of related items.
|
||||
return [self.to_native(item.pk) for item in queryset]
|
||||
|
||||
# To-one relationship
|
||||
try:
|
||||
# Prefer obj.serializable_value for performance reasons
|
||||
pk = obj.serializable_value(self.source or field_name)
|
||||
except AttributeError:
|
||||
# RelatedObject (reverse relationship)
|
||||
try:
|
||||
pk = getattr(obj, self.source or field_name).pk
|
||||
except (ObjectDoesNotExist, AttributeError):
|
||||
return None
|
||||
|
||||
# Forward relationship
|
||||
return self.to_native(pk)
|
||||
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a relationship using a unique field on the target.
|
||||
"""
|
||||
read_only = False
|
||||
|
||||
default_error_messages = {
|
||||
"does_not_exist": _("Object with %s=%s does not exist."),
|
||||
"invalid": _("Invalid value."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.slug_field = kwargs.pop("slug_field", None)
|
||||
assert self.slug_field, "slug_field is required"
|
||||
super(SlugRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_native(self, obj):
|
||||
return getattr(obj, self.slug_field)
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception("Writable related fields must include a `queryset` argument")
|
||||
|
||||
try:
|
||||
return self.queryset.get(**{self.slug_field: data})
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError(self.error_messages["does_not_exist"] %
|
||||
(self.slug_field, smart_text(data)))
|
||||
except (TypeError, ValueError):
|
||||
msg = self.error_messages["invalid"]
|
||||
raise ValidationError(msg)
|
||||
|
||||
|
||||
### Hyperlinked relationships
|
||||
|
||||
class HyperlinkedRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a relationship using hyperlinking.
|
||||
"""
|
||||
read_only = False
|
||||
lookup_field = "pk"
|
||||
|
||||
default_error_messages = {
|
||||
"no_match": _("Invalid hyperlink - No URL match"),
|
||||
"incorrect_match": _("Invalid hyperlink - Incorrect URL match"),
|
||||
"configuration_error": _("Invalid hyperlink due to configuration error"),
|
||||
"does_not_exist": _("Invalid hyperlink - object does not exist."),
|
||||
"incorrect_type": _("Incorrect type. Expected url string, received %s."),
|
||||
}
|
||||
|
||||
# These are all pending deprecation
|
||||
pk_url_kwarg = "pk"
|
||||
slug_field = "slug"
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop("view_name")
|
||||
except KeyError:
|
||||
raise ValueError("Hyperlinked field requires \"view_name\" kwarg")
|
||||
|
||||
self.lookup_field = kwargs.pop("lookup_field", self.lookup_field)
|
||||
self.format = kwargs.pop("format", None)
|
||||
|
||||
# These are pending deprecation
|
||||
if "pk_url_kwarg" in kwargs:
|
||||
msg = "pk_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
if "slug_url_kwarg" in kwargs:
|
||||
msg = "slug_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
if "slug_field" in kwargs:
|
||||
msg = "slug_field is pending deprecation. Use lookup_field instead."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
self.pk_url_kwarg = kwargs.pop("pk_url_kwarg", self.pk_url_kwarg)
|
||||
self.slug_field = kwargs.pop("slug_field", self.slug_field)
|
||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||
self.slug_url_kwarg = kwargs.pop("slug_url_kwarg", default_slug_kwarg)
|
||||
|
||||
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_url(self, obj, view_name, request, format):
|
||||
"""
|
||||
Given an object, return the URL that hyperlinks to the object.
|
||||
|
||||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||
attributes are not configured to correctly match the URL conf.
|
||||
"""
|
||||
lookup_field = getattr(obj, self.lookup_field)
|
||||
kwargs = {self.lookup_field: lookup_field}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
if self.pk_url_kwarg != "pk":
|
||||
# Only try pk if it has been explicitly set.
|
||||
# Otherwise, the default `lookup_field = "pk"` has us covered.
|
||||
pk = obj.pk
|
||||
kwargs = {self.pk_url_kwarg: pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
if slug is not None:
|
||||
# Only try slug if it corresponds to an attribute on the object.
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
if self.slug_field == "slug" and self.slug_url_kwarg == "slug":
|
||||
# If the lookup succeeds using the default slug params,
|
||||
# then `slug_field` is being used implicitly, and we
|
||||
# we need to warn about the pending deprecation.
|
||||
msg = "Implicit slug field hyperlinked fields are pending deprecation." \
|
||||
"You should set `lookup_field=slug` on the HyperlinkedRelatedField."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
return ret
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
raise NoReverseMatch()
|
||||
|
||||
def get_object(self, queryset, view_name, view_args, view_kwargs):
|
||||
"""
|
||||
Return the object corresponding to a matched URL.
|
||||
|
||||
Takes the matched URL conf arguments, and the queryset, and should
|
||||
return an object instance, or raise an `ObjectDoesNotExist` exception.
|
||||
"""
|
||||
lookup = view_kwargs.get(self.lookup_field, None)
|
||||
pk = view_kwargs.get(self.pk_url_kwarg, None)
|
||||
slug = view_kwargs.get(self.slug_url_kwarg, None)
|
||||
|
||||
if lookup is not None:
|
||||
filter_kwargs = {self.lookup_field: lookup}
|
||||
elif pk is not None:
|
||||
filter_kwargs = {"pk": pk}
|
||||
elif slug is not None:
|
||||
filter_kwargs = {self.slug_field: slug}
|
||||
else:
|
||||
raise ObjectDoesNotExist()
|
||||
|
||||
return queryset.get(**filter_kwargs)
|
||||
|
||||
def to_native(self, obj):
|
||||
view_name = self.view_name
|
||||
request = self.context.get("request", None)
|
||||
format = self.format or self.context.get("format", None)
|
||||
|
||||
if request is None:
|
||||
msg = (
|
||||
"Using `HyperlinkedRelatedField` without including the request "
|
||||
"in the serializer context is deprecated. "
|
||||
"Add `context={'request': request}` when instantiating "
|
||||
"the serializer."
|
||||
)
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=4)
|
||||
|
||||
# If the object has not yet been saved then we cannot hyperlink to it.
|
||||
if getattr(obj, "pk", None) is None:
|
||||
return
|
||||
|
||||
# Return the hyperlink, or error if incorrectly configured.
|
||||
try:
|
||||
return self.get_url(obj, view_name, request, format)
|
||||
except NoReverseMatch:
|
||||
msg = (
|
||||
"Could not resolve URL for hyperlinked relationship using "
|
||||
"view name '%s'. You may have failed to include the related "
|
||||
"model in your API, or incorrectly configured the "
|
||||
"`lookup_field` attribute on this field."
|
||||
)
|
||||
raise Exception(msg % view_name)
|
||||
|
||||
def from_native(self, value):
|
||||
# Convert URL -> model instance pk
|
||||
# TODO: Use values_list
|
||||
queryset = self.queryset
|
||||
if queryset is None:
|
||||
raise Exception("Writable related fields must include a `queryset` argument")
|
||||
|
||||
try:
|
||||
http_prefix = value.startswith(("http:", "https:"))
|
||||
except AttributeError:
|
||||
msg = self.error_messages["incorrect_type"]
|
||||
raise ValidationError(msg % type(value).__name__)
|
||||
|
||||
if http_prefix:
|
||||
# If needed convert absolute URLs to relative path
|
||||
value = urlparse.urlparse(value).path
|
||||
prefix = get_script_prefix()
|
||||
if value.startswith(prefix):
|
||||
value = "/" + value[len(prefix):]
|
||||
|
||||
try:
|
||||
match = resolve(value)
|
||||
except Exception:
|
||||
raise ValidationError(self.error_messages["no_match"])
|
||||
|
||||
if match.view_name != self.view_name:
|
||||
raise ValidationError(self.error_messages["incorrect_match"])
|
||||
|
||||
try:
|
||||
return self.get_object(queryset, match.view_name,
|
||||
match.args, match.kwargs)
|
||||
except (ObjectDoesNotExist, TypeError, ValueError):
|
||||
raise ValidationError(self.error_messages["does_not_exist"])
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
"""
|
||||
Represents the instance, or a property on the instance, using hyperlinking.
|
||||
"""
|
||||
lookup_field = "pk"
|
||||
read_only = True
|
||||
|
||||
# These are all pending deprecation
|
||||
pk_url_kwarg = "pk"
|
||||
slug_field = "slug"
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.view_name = kwargs.pop("view_name")
|
||||
except KeyError:
|
||||
msg = "HyperlinkedIdentityField requires \"view_name\" argument"
|
||||
raise ValueError(msg)
|
||||
|
||||
self.format = kwargs.pop("format", None)
|
||||
lookup_field = kwargs.pop("lookup_field", None)
|
||||
self.lookup_field = lookup_field or self.lookup_field
|
||||
|
||||
# These are pending deprecation
|
||||
if "pk_url_kwarg" in kwargs:
|
||||
msg = "pk_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
if "slug_url_kwarg" in kwargs:
|
||||
msg = "slug_url_kwarg is pending deprecation. Use lookup_field instead."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
if "slug_field" in kwargs:
|
||||
msg = "slug_field is pending deprecation. Use lookup_field instead."
|
||||
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
|
||||
|
||||
self.slug_field = kwargs.pop("slug_field", self.slug_field)
|
||||
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
|
||||
self.pk_url_kwarg = kwargs.pop("pk_url_kwarg", self.pk_url_kwarg)
|
||||
self.slug_url_kwarg = kwargs.pop("slug_url_kwarg", default_slug_kwarg)
|
||||
|
||||
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
request = self.context.get("request", None)
|
||||
format = self.context.get("format", None)
|
||||
view_name = self.view_name
|
||||
|
||||
if request is None:
|
||||
warnings.warn("Using `HyperlinkedIdentityField` without including the "
|
||||
"request in the serializer context is deprecated. "
|
||||
"Add `context={'request': request}` when instantiating the serializer.",
|
||||
DeprecationWarning, stacklevel=4)
|
||||
|
||||
# By default use whatever format is given for the current context
|
||||
# unless the target is a different type to the source.
|
||||
#
|
||||
# Eg. Consider a HyperlinkedIdentityField pointing from a json
|
||||
# representation to an html property of that representation...
|
||||
#
|
||||
# "/snippets/1/" should link to "/snippets/1/highlight/"
|
||||
# ...but...
|
||||
# "/snippets/1/.json" should link to "/snippets/1/highlight/.html"
|
||||
if format and self.format and self.format != format:
|
||||
format = self.format
|
||||
|
||||
# Return the hyperlink, or error if incorrectly configured.
|
||||
try:
|
||||
return self.get_url(obj, view_name, request, format)
|
||||
except NoReverseMatch:
|
||||
msg = (
|
||||
"Could not resolve URL for hyperlinked relationship using "
|
||||
"view name '%s'. You may have failed to include the related "
|
||||
"model in your API, or incorrectly configured the "
|
||||
"`lookup_field` attribute on this field."
|
||||
)
|
||||
raise Exception(msg % view_name)
|
||||
|
||||
def get_url(self, obj, view_name, request, format):
|
||||
"""
|
||||
Given an object, return the URL that hyperlinks to the object.
|
||||
|
||||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
||||
attributes are not configured to correctly match the URL conf.
|
||||
"""
|
||||
lookup_field = getattr(obj, self.lookup_field, None)
|
||||
kwargs = {self.lookup_field: lookup_field}
|
||||
|
||||
# Handle unsaved object case
|
||||
if lookup_field is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
if self.pk_url_kwarg != "pk":
|
||||
# Only try pk lookup if it has been explicitly set.
|
||||
# Otherwise, the default `lookup_field = "pk"` has us covered.
|
||||
kwargs = {self.pk_url_kwarg: obj.pk}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
slug = getattr(obj, self.slug_field, None)
|
||||
if slug:
|
||||
# Only use slug lookup if a slug field exists on the model
|
||||
kwargs = {self.slug_url_kwarg: slug}
|
||||
try:
|
||||
return reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
raise NoReverseMatch()
|
|
@ -0,0 +1,613 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Renderers are used to serialize a response into specific media types.
|
||||
|
||||
They give us a generic way of being able to handle various media types
|
||||
on the response, such as JSON encoded data or HTML output.
|
||||
|
||||
REST framework also provides an HTML renderer the renders the browsable API.
|
||||
"""
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.template import RequestContext, loader, Template
|
||||
from django.test.client import encode_multipart
|
||||
from django.utils import six
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils.six import StringIO
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
|
||||
from taiga.base import exceptions, status
|
||||
from taiga.base.exceptions import ParseError
|
||||
|
||||
from . import VERSION
|
||||
from .request import is_form_media_type, override_method
|
||||
from .settings import api_settings
|
||||
from .utils import encoders
|
||||
from .utils.breadcrumbs import get_breadcrumbs
|
||||
|
||||
import json
|
||||
import copy
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
"""
|
||||
All renderers should extend this class, setting the `media_type`
|
||||
and `format` attributes, and override the `.render()` method.
|
||||
"""
|
||||
|
||||
media_type = None
|
||||
format = None
|
||||
charset = "utf-8"
|
||||
render_style = "text"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
raise NotImplemented("Renderer class requires .render() to be implemented")
|
||||
|
||||
|
||||
class JSONRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderer which serializes to JSON.
|
||||
Applies JSON's backslash-u character escaping for non-ascii characters.
|
||||
"""
|
||||
|
||||
media_type = "application/json"
|
||||
format = "json"
|
||||
encoder_class = encoders.JSONEncoder
|
||||
ensure_ascii = True
|
||||
charset = None
|
||||
# JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32.
|
||||
# See: http://www.ietf.org/rfc/rfc4627.txt
|
||||
# Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Render `data` into JSON.
|
||||
"""
|
||||
if data is None:
|
||||
return bytes()
|
||||
|
||||
# If "indent" is provided in the context, then pretty print the result.
|
||||
# E.g. If we"re being called by the BrowsableAPIRenderer.
|
||||
renderer_context = renderer_context or {}
|
||||
indent = renderer_context.get("indent", None)
|
||||
|
||||
if accepted_media_type:
|
||||
# If the media type looks like "application/json; indent=4",
|
||||
# then pretty print the result.
|
||||
base_media_type, params = parse_header(accepted_media_type.encode("ascii"))
|
||||
indent = params.get("indent", indent)
|
||||
try:
|
||||
indent = max(min(int(indent), 8), 0)
|
||||
except (ValueError, TypeError):
|
||||
indent = None
|
||||
|
||||
ret = json.dumps(data, cls=self.encoder_class,
|
||||
indent=indent, ensure_ascii=self.ensure_ascii)
|
||||
|
||||
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
|
||||
# but if ensure_ascii=False, the return type is underspecified,
|
||||
# and may (or may not) be unicode.
|
||||
# On python 3.x json.dumps() returns unicode strings.
|
||||
if isinstance(ret, six.text_type):
|
||||
return bytes(ret.encode("utf-8"))
|
||||
return ret
|
||||
|
||||
|
||||
class UnicodeJSONRenderer(JSONRenderer):
|
||||
ensure_ascii = False
|
||||
"""
|
||||
Renderer which serializes to JSON.
|
||||
Does *not* apply JSON's character escaping for non-ascii characters.
|
||||
"""
|
||||
|
||||
|
||||
class JSONPRenderer(JSONRenderer):
|
||||
"""
|
||||
Renderer which serializes to json,
|
||||
wrapping the json output in a callback function.
|
||||
"""
|
||||
|
||||
media_type = "application/javascript"
|
||||
format = "jsonp"
|
||||
callback_parameter = "callback"
|
||||
default_callback = "callback"
|
||||
charset = "utf-8"
|
||||
|
||||
def get_callback(self, renderer_context):
|
||||
"""
|
||||
Determine the name of the callback to wrap around the json output.
|
||||
"""
|
||||
request = renderer_context.get("request", None)
|
||||
params = request and request.QUERY_PARAMS or {}
|
||||
return params.get(self.callback_parameter, self.default_callback)
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders into jsonp, wrapping the json output in a callback function.
|
||||
|
||||
Clients may set the callback function name using a query parameter
|
||||
on the URL, for example: ?callback=exampleCallbackName
|
||||
"""
|
||||
renderer_context = renderer_context or {}
|
||||
callback = self.get_callback(renderer_context)
|
||||
json = super(JSONPRenderer, self).render(data, accepted_media_type,
|
||||
renderer_context)
|
||||
return callback.encode(self.charset) + b"(" + json + b");"
|
||||
|
||||
|
||||
class XMLRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderer which serializes to XML.
|
||||
"""
|
||||
|
||||
media_type = "application/xml"
|
||||
format = "xml"
|
||||
charset = "utf-8"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders `data` into serialized XML.
|
||||
"""
|
||||
if data is None:
|
||||
return ""
|
||||
|
||||
stream = StringIO()
|
||||
|
||||
xml = SimplerXMLGenerator(stream, self.charset)
|
||||
xml.startDocument()
|
||||
xml.startElement("root", {})
|
||||
|
||||
self._to_xml(xml, data)
|
||||
|
||||
xml.endElement("root")
|
||||
xml.endDocument()
|
||||
return stream.getvalue()
|
||||
|
||||
def _to_xml(self, xml, data):
|
||||
if isinstance(data, (list, tuple)):
|
||||
for item in data:
|
||||
xml.startElement("list-item", {})
|
||||
self._to_xml(xml, item)
|
||||
xml.endElement("list-item")
|
||||
|
||||
elif isinstance(data, dict):
|
||||
for key, value in six.iteritems(data):
|
||||
xml.startElement(key, {})
|
||||
self._to_xml(xml, value)
|
||||
xml.endElement(key)
|
||||
|
||||
elif data is None:
|
||||
# Don't output any value
|
||||
pass
|
||||
|
||||
else:
|
||||
xml.characters(smart_text(data))
|
||||
|
||||
|
||||
class TemplateHTMLRenderer(BaseRenderer):
|
||||
"""
|
||||
An HTML renderer for use with templates.
|
||||
|
||||
The data supplied to the Response object should be a dictionary that will
|
||||
be used as context for the template.
|
||||
|
||||
The template name is determined by (in order of preference):
|
||||
|
||||
1. An explicit `.template_name` attribute set on the response.
|
||||
2. An explicit `.template_name` attribute set on this class.
|
||||
3. The return result of calling `view.get_template_names()`.
|
||||
|
||||
For example:
|
||||
data = {"users": User.objects.all()}
|
||||
return Response(data, template_name="users.html")
|
||||
|
||||
For pre-rendered HTML, see StaticHTMLRenderer.
|
||||
"""
|
||||
|
||||
media_type = "text/html"
|
||||
format = "html"
|
||||
template_name = None
|
||||
exception_template_names = [
|
||||
"%(status_code)s.html",
|
||||
"api_exception.html"
|
||||
]
|
||||
charset = "utf-8"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Renders data to HTML, using Django's standard template rendering.
|
||||
|
||||
The template name is determined by (in order of preference):
|
||||
|
||||
1. An explicit .template_name set on the response.
|
||||
2. An explicit .template_name set on this class.
|
||||
3. The return result of calling view.get_template_names().
|
||||
"""
|
||||
renderer_context = renderer_context or {}
|
||||
view = renderer_context["view"]
|
||||
request = renderer_context["request"]
|
||||
response = renderer_context["response"]
|
||||
|
||||
if response.exception:
|
||||
template = self.get_exception_template(response)
|
||||
else:
|
||||
template_names = self.get_template_names(response, view)
|
||||
template = self.resolve_template(template_names)
|
||||
|
||||
context = self.resolve_context(data, request, response)
|
||||
return template.render(context)
|
||||
|
||||
def resolve_template(self, template_names):
|
||||
return loader.select_template(template_names)
|
||||
|
||||
def resolve_context(self, data, request, response):
|
||||
if response.exception:
|
||||
data["status_code"] = response.status_code
|
||||
return RequestContext(request, data)
|
||||
|
||||
def get_template_names(self, response, view):
|
||||
if response.template_name:
|
||||
return [response.template_name]
|
||||
elif self.template_name:
|
||||
return [self.template_name]
|
||||
elif hasattr(view, "get_template_names"):
|
||||
return view.get_template_names()
|
||||
elif hasattr(view, "template_name"):
|
||||
return [view.template_name]
|
||||
raise ImproperlyConfigured("Returned a template response with no `template_name` attribute set on either the view or response")
|
||||
|
||||
def get_exception_template(self, response):
|
||||
template_names = [name % {"status_code": response.status_code}
|
||||
for name in self.exception_template_names]
|
||||
|
||||
try:
|
||||
# Try to find an appropriate error template
|
||||
return self.resolve_template(template_names)
|
||||
except Exception:
|
||||
# Fall back to using eg "404 Not Found"
|
||||
return Template("%d %s" % (response.status_code,
|
||||
response.status_text.title()))
|
||||
|
||||
|
||||
# Note, subclass TemplateHTMLRenderer simply for the exception behavior
|
||||
class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||
"""
|
||||
An HTML renderer class that simply returns pre-rendered HTML.
|
||||
|
||||
The data supplied to the Response object should be a string representing
|
||||
the pre-rendered HTML content.
|
||||
|
||||
For example:
|
||||
data = "<html><body>example</body></html>"
|
||||
return Response(data)
|
||||
|
||||
For template rendered HTML, see TemplateHTMLRenderer.
|
||||
"""
|
||||
media_type = "text/html"
|
||||
format = "html"
|
||||
charset = "utf-8"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
renderer_context = renderer_context or {}
|
||||
response = renderer_context["response"]
|
||||
|
||||
if response and response.exception:
|
||||
request = renderer_context["request"]
|
||||
template = self.get_exception_template(response)
|
||||
context = self.resolve_context(data, request, response)
|
||||
return template.render(context)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class HTMLFormRenderer(BaseRenderer):
|
||||
"""
|
||||
Renderers serializer data into an HTML form.
|
||||
|
||||
If the serializer was instantiated without an object then this will
|
||||
return an HTML form not bound to any object,
|
||||
otherwise it will return an HTML form with the appropriate initial data
|
||||
populated from the object.
|
||||
|
||||
Note that rendering of field and form errors is not currently supported.
|
||||
"""
|
||||
media_type = "text/html"
|
||||
format = "form"
|
||||
template = "api/form.html"
|
||||
charset = "utf-8"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Render serializer data and return an HTML form, as a string.
|
||||
"""
|
||||
renderer_context = renderer_context or {}
|
||||
request = renderer_context["request"]
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = RequestContext(request, {"form": data})
|
||||
return template.render(context)
|
||||
|
||||
|
||||
class BrowsableAPIRenderer(BaseRenderer):
|
||||
"""
|
||||
HTML renderer used to self-document the API.
|
||||
"""
|
||||
media_type = "text/html"
|
||||
format = "api"
|
||||
template = "api/api.html"
|
||||
charset = "utf-8"
|
||||
form_renderer_class = HTMLFormRenderer
|
||||
|
||||
def get_default_renderer(self, view):
|
||||
"""
|
||||
Return an instance of the first valid renderer.
|
||||
(Don't use another documenting renderer.)
|
||||
"""
|
||||
renderers = [renderer for renderer in view.renderer_classes
|
||||
if not issubclass(renderer, BrowsableAPIRenderer)]
|
||||
non_template_renderers = [renderer for renderer in renderers
|
||||
if not hasattr(renderer, "get_template_names")]
|
||||
|
||||
if not renderers:
|
||||
return None
|
||||
elif non_template_renderers:
|
||||
return non_template_renderers[0]()
|
||||
return renderers[0]()
|
||||
|
||||
def get_content(self, renderer, data,
|
||||
accepted_media_type, renderer_context):
|
||||
"""
|
||||
Get the content as if it had been rendered by the default
|
||||
non-documenting renderer.
|
||||
"""
|
||||
if not renderer:
|
||||
return "[No renderers were found]"
|
||||
|
||||
renderer_context["indent"] = 4
|
||||
content = renderer.render(data, accepted_media_type, renderer_context)
|
||||
|
||||
render_style = getattr(renderer, "render_style", "text")
|
||||
assert render_style in ["text", "binary"], 'Expected .render_style "text" or "binary", ' \
|
||||
'but got "%s"' % render_style
|
||||
if render_style == "binary":
|
||||
return "[%d bytes of binary content]" % len(content)
|
||||
|
||||
return content
|
||||
|
||||
def show_form_for_method(self, view, method, request, obj):
|
||||
"""
|
||||
Returns True if a form should be shown for this method.
|
||||
"""
|
||||
if not method in view.allowed_methods:
|
||||
return # Not a valid method
|
||||
|
||||
if not api_settings.FORM_METHOD_OVERRIDE:
|
||||
return # Cannot use form overloading
|
||||
|
||||
try:
|
||||
view.check_permissions(request)
|
||||
if obj is not None:
|
||||
view.check_object_permissions(request, obj)
|
||||
except exceptions.APIException:
|
||||
return False # Doesn't have permissions
|
||||
return True
|
||||
|
||||
def get_rendered_html_form(self, view, method, request):
|
||||
"""
|
||||
Return a string representing a rendered HTML form, possibly bound to
|
||||
either the input or output data.
|
||||
|
||||
In the absence of the View having an associated form then return None.
|
||||
"""
|
||||
if request.method == method:
|
||||
try:
|
||||
data = request.DATA
|
||||
files = request.FILES
|
||||
except ParseError:
|
||||
data = None
|
||||
files = None
|
||||
else:
|
||||
data = None
|
||||
files = None
|
||||
|
||||
with override_method(view, request, method) as request:
|
||||
obj = getattr(view, "object", None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
|
||||
if method in ("DELETE", "OPTIONS"):
|
||||
return True # Don't actually need to return a form
|
||||
|
||||
if (not getattr(view, "get_serializer", None)
|
||||
or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)):
|
||||
return
|
||||
|
||||
serializer = view.get_serializer(instance=obj, data=data, files=files)
|
||||
serializer.is_valid()
|
||||
data = serializer.data
|
||||
|
||||
form_renderer = self.form_renderer_class()
|
||||
return form_renderer.render(data, self.accepted_media_type, self.renderer_context)
|
||||
|
||||
def get_raw_data_form(self, view, method, request):
|
||||
"""
|
||||
Returns a form that allows for arbitrary content types to be tunneled
|
||||
via standard HTML forms.
|
||||
(Which are typically application/x-www-form-urlencoded)
|
||||
"""
|
||||
with override_method(view, request, method) as request:
|
||||
# If we"re not using content overloading there's no point in
|
||||
# supplying a generic form, as the view won't treat the form"s
|
||||
# value as the content of the request.
|
||||
if not (api_settings.FORM_CONTENT_OVERRIDE
|
||||
and api_settings.FORM_CONTENTTYPE_OVERRIDE):
|
||||
return None
|
||||
|
||||
# Check permissions
|
||||
obj = getattr(view, "object", None)
|
||||
if not self.show_form_for_method(view, method, request, obj):
|
||||
return
|
||||
|
||||
# If possible, serialize the initial content for the generic form
|
||||
default_parser = view.parser_classes[0]
|
||||
renderer_class = getattr(default_parser, "renderer_class", None)
|
||||
if (hasattr(view, "get_serializer") and renderer_class):
|
||||
# View has a serializer defined and parser class has a
|
||||
# corresponding renderer that can be used to render the data.
|
||||
|
||||
# Get a read-only version of the serializer
|
||||
serializer = view.get_serializer(instance=obj)
|
||||
if obj is None:
|
||||
for name, field in serializer.fields.items():
|
||||
if getattr(field, "read_only", None):
|
||||
del serializer.fields[name]
|
||||
|
||||
# Render the raw data content
|
||||
renderer = renderer_class()
|
||||
accepted = self.accepted_media_type
|
||||
context = self.renderer_context.copy()
|
||||
context["indent"] = 4
|
||||
content = renderer.render(serializer.data, accepted, context)
|
||||
else:
|
||||
content = None
|
||||
|
||||
# Generate a generic form that includes a content type field,
|
||||
# and a content field.
|
||||
content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||
content_field = api_settings.FORM_CONTENT_OVERRIDE
|
||||
|
||||
media_types = [parser.media_type for parser in view.parser_classes]
|
||||
choices = [(media_type, media_type) for media_type in media_types]
|
||||
initial = media_types[0]
|
||||
|
||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
||||
class GenericContentForm(forms.Form):
|
||||
def __init__(self):
|
||||
super(GenericContentForm, self).__init__()
|
||||
|
||||
self.fields[content_type_field] = forms.ChoiceField(
|
||||
label="Media type",
|
||||
choices=choices,
|
||||
initial=initial
|
||||
)
|
||||
self.fields[content_field] = forms.CharField(
|
||||
label="Content",
|
||||
widget=forms.Textarea,
|
||||
initial=content
|
||||
)
|
||||
|
||||
return GenericContentForm()
|
||||
|
||||
def get_name(self, view):
|
||||
return view.get_view_name()
|
||||
|
||||
def get_description(self, view):
|
||||
return view.get_view_description(html=True)
|
||||
|
||||
def get_breadcrumbs(self, request):
|
||||
return get_breadcrumbs(request.path)
|
||||
|
||||
def get_context(self, data, accepted_media_type, renderer_context):
|
||||
"""
|
||||
Returns the context used to render.
|
||||
"""
|
||||
view = renderer_context["view"]
|
||||
request = renderer_context["request"]
|
||||
response = renderer_context["response"]
|
||||
|
||||
renderer = self.get_default_renderer(view)
|
||||
|
||||
raw_data_post_form = self.get_raw_data_form(view, "POST", request)
|
||||
raw_data_put_form = self.get_raw_data_form(view, "PUT", request)
|
||||
raw_data_patch_form = self.get_raw_data_form(view, "PATCH", request)
|
||||
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
|
||||
|
||||
response_headers = dict(response.items())
|
||||
renderer_content_type = ""
|
||||
if renderer:
|
||||
renderer_content_type = "%s" % renderer.media_type
|
||||
if renderer.charset:
|
||||
renderer_content_type += " ;%s" % renderer.charset
|
||||
response_headers["Content-Type"] = renderer_content_type
|
||||
|
||||
context = {
|
||||
"content": self.get_content(renderer, data, accepted_media_type, renderer_context),
|
||||
"view": view,
|
||||
"request": request,
|
||||
"response": response,
|
||||
"description": self.get_description(view),
|
||||
"name": self.get_name(view),
|
||||
"version": VERSION,
|
||||
"breadcrumblist": self.get_breadcrumbs(request),
|
||||
"allowed_methods": view.allowed_methods,
|
||||
"available_formats": [renderer.format for renderer in view.renderer_classes],
|
||||
"response_headers": response_headers,
|
||||
|
||||
"put_form": self.get_rendered_html_form(view, "PUT", request),
|
||||
"post_form": self.get_rendered_html_form(view, "POST", request),
|
||||
"delete_form": self.get_rendered_html_form(view, "DELETE", request),
|
||||
"options_form": self.get_rendered_html_form(view, "OPTIONS", request),
|
||||
|
||||
"raw_data_put_form": raw_data_put_form,
|
||||
"raw_data_post_form": raw_data_post_form,
|
||||
"raw_data_patch_form": raw_data_patch_form,
|
||||
"raw_data_put_or_patch_form": raw_data_put_or_patch_form,
|
||||
|
||||
"display_edit_forms": bool(response.status_code != 403),
|
||||
|
||||
"api_settings": api_settings
|
||||
}
|
||||
return context
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
Render the HTML for the browsable API representation.
|
||||
"""
|
||||
self.accepted_media_type = accepted_media_type or ""
|
||||
self.renderer_context = renderer_context or {}
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = self.get_context(data, accepted_media_type, renderer_context)
|
||||
context = RequestContext(renderer_context["request"], context)
|
||||
ret = template.render(context)
|
||||
|
||||
# Munge DELETE Response code to allow us to return content
|
||||
# (Do this *after* we"ve rendered the template so that we include
|
||||
# the normal deletion response code in the output)
|
||||
response = renderer_context["response"]
|
||||
if response.status_code == status.HTTP_204_NO_CONTENT:
|
||||
response.status_code = status.HTTP_200_OK
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class MultiPartRenderer(BaseRenderer):
|
||||
media_type = "multipart/form-data; boundary=BoUnDaRyStRiNg"
|
||||
format = "multipart"
|
||||
charset = "utf-8"
|
||||
BOUNDARY = "BoUnDaRyStRiNg"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
return encode_multipart(self.BOUNDARY, data)
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
The Request class is used as a wrapper around the standard request object.
|
||||
|
||||
The wrapped request then offers a richer API, in particular :
|
||||
|
||||
- content automatically parsed according to `Content-Type` header,
|
||||
and available as `request.DATA`
|
||||
- full support of PUT method, including support for file uploads
|
||||
- form overloading of HTTP method, content type and content
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
from django.http.multipartparser import parse_header
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.six import BytesIO
|
||||
|
||||
from taiga.base import exceptions
|
||||
|
||||
from . import HTTP_HEADER_ENCODING
|
||||
from .settings import api_settings
|
||||
|
||||
|
||||
def is_form_media_type(media_type):
|
||||
"""
|
||||
Return True if the media type is a valid form media type.
|
||||
"""
|
||||
base_media_type, params = parse_header(media_type.encode(HTTP_HEADER_ENCODING))
|
||||
return (base_media_type == "application/x-www-form-urlencoded" or
|
||||
base_media_type == "multipart/form-data")
|
||||
|
||||
|
||||
class override_method(object):
|
||||
"""
|
||||
A context manager that temporarily overrides the method on a request,
|
||||
additionally setting the `view.request` attribute.
|
||||
|
||||
Usage:
|
||||
|
||||
with override_method(view, request, "POST") as request:
|
||||
... # Do stuff with `view` and `request`
|
||||
"""
|
||||
def __init__(self, view, request, method):
|
||||
self.view = view
|
||||
self.request = request
|
||||
self.method = method
|
||||
|
||||
def __enter__(self):
|
||||
self.view.request = clone_request(self.request, self.method)
|
||||
return self.view.request
|
||||
|
||||
def __exit__(self, *args, **kwarg):
|
||||
self.view.request = self.request
|
||||
|
||||
|
||||
class Empty(object):
|
||||
"""
|
||||
Placeholder for unset attributes.
|
||||
Cannot use `None`, as that may be a valid value.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _hasattr(obj, name):
|
||||
return not getattr(obj, name) is Empty
|
||||
|
||||
|
||||
def clone_request(request, method):
|
||||
"""
|
||||
Internal helper method to clone a request, replacing with a different
|
||||
HTTP method. Used for checking permissions against other methods.
|
||||
"""
|
||||
ret = Request(request=request._request,
|
||||
parsers=request.parsers,
|
||||
authenticators=request.authenticators,
|
||||
negotiator=request.negotiator,
|
||||
parser_context=request.parser_context)
|
||||
ret._data = request._data
|
||||
ret._files = request._files
|
||||
ret._content_type = request._content_type
|
||||
ret._stream = request._stream
|
||||
ret._method = method
|
||||
if hasattr(request, "_user"):
|
||||
ret._user = request._user
|
||||
if hasattr(request, "_auth"):
|
||||
ret._auth = request._auth
|
||||
if hasattr(request, "_authenticator"):
|
||||
ret._authenticator = request._authenticator
|
||||
return ret
|
||||
|
||||
|
||||
class ForcedAuthentication(object):
|
||||
"""
|
||||
This authentication class is used if the test client or request factory
|
||||
forcibly authenticated the request.
|
||||
"""
|
||||
|
||||
def __init__(self, force_user, force_token):
|
||||
self.force_user = force_user
|
||||
self.force_token = force_token
|
||||
|
||||
def authenticate(self, request):
|
||||
return (self.force_user, self.force_token)
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
Wrapper allowing to enhance a standard `HttpRequest` instance.
|
||||
|
||||
Kwargs:
|
||||
- request(HttpRequest). The original request instance.
|
||||
- parsers_classes(list/tuple). The parsers to use for parsing the
|
||||
request content.
|
||||
- authentication_classes(list/tuple). The authentications used to try
|
||||
authenticating the request's user.
|
||||
"""
|
||||
|
||||
_METHOD_PARAM = api_settings.FORM_METHOD_OVERRIDE
|
||||
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
|
||||
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
|
||||
|
||||
def __init__(self, request, parsers=None, authenticators=None,
|
||||
negotiator=None, parser_context=None):
|
||||
self._request = request
|
||||
self.parsers = parsers or ()
|
||||
self.authenticators = authenticators or ()
|
||||
self.negotiator = negotiator or self._default_negotiator()
|
||||
self.parser_context = parser_context
|
||||
self._data = Empty
|
||||
self._files = Empty
|
||||
self._method = Empty
|
||||
self._content_type = Empty
|
||||
self._stream = Empty
|
||||
|
||||
if self.parser_context is None:
|
||||
self.parser_context = {}
|
||||
self.parser_context["request"] = self
|
||||
self.parser_context["encoding"] = request.encoding or settings.DEFAULT_CHARSET
|
||||
|
||||
force_user = getattr(request, "_force_auth_user", None)
|
||||
force_token = getattr(request, "_force_auth_token", None)
|
||||
if (force_user is not None or force_token is not None):
|
||||
forced_auth = ForcedAuthentication(force_user, force_token)
|
||||
self.authenticators = (forced_auth,)
|
||||
|
||||
def _default_negotiator(self):
|
||||
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
"""
|
||||
Returns the HTTP method.
|
||||
|
||||
This allows the `method` to be overridden by using a hidden `form`
|
||||
field on a form POST request.
|
||||
"""
|
||||
if not _hasattr(self, "_method"):
|
||||
self._load_method_and_content_type()
|
||||
return self._method
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
"""
|
||||
Returns the content type header.
|
||||
|
||||
This should be used instead of `request.META.get("HTTP_CONTENT_TYPE")`,
|
||||
as it allows the content type to be overridden by using a hidden form
|
||||
field on a form POST request.
|
||||
"""
|
||||
if not _hasattr(self, "_content_type"):
|
||||
self._load_method_and_content_type()
|
||||
return self._content_type
|
||||
|
||||
@property
|
||||
def stream(self):
|
||||
"""
|
||||
Returns an object that may be used to stream the request content.
|
||||
"""
|
||||
if not _hasattr(self, "_stream"):
|
||||
self._load_stream()
|
||||
return self._stream
|
||||
|
||||
@property
|
||||
def QUERY_PARAMS(self):
|
||||
"""
|
||||
More semantically correct name for request.GET.
|
||||
"""
|
||||
return self._request.GET
|
||||
|
||||
@property
|
||||
def DATA(self):
|
||||
"""
|
||||
Parses the request body and returns the data.
|
||||
|
||||
Similar to usual behaviour of `request.POST`, except that it handles
|
||||
arbitrary parsers, and also works on methods other than POST (eg PUT).
|
||||
"""
|
||||
if not _hasattr(self, "_data"):
|
||||
self._load_data_and_files()
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def FILES(self):
|
||||
"""
|
||||
Parses the request body and returns any files uploaded in the request.
|
||||
|
||||
Similar to usual behaviour of `request.FILES`, except that it handles
|
||||
arbitrary parsers, and also works on methods other than POST (eg PUT).
|
||||
"""
|
||||
if not _hasattr(self, "_files"):
|
||||
self._load_data_and_files()
|
||||
return self._files
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
"""
|
||||
Returns the user associated with the current request, as authenticated
|
||||
by the authentication classes provided to the request.
|
||||
"""
|
||||
if not hasattr(self, "_user"):
|
||||
self._authenticate()
|
||||
return self._user
|
||||
|
||||
@user.setter
|
||||
def user(self, value):
|
||||
"""
|
||||
Sets the user on the current request. This is necessary to maintain
|
||||
compatibility with django.contrib.auth where the user property is
|
||||
set in the login and logout functions.
|
||||
"""
|
||||
self._user = value
|
||||
|
||||
@property
|
||||
def auth(self):
|
||||
"""
|
||||
Returns any non-user authentication information associated with the
|
||||
request, such as an authentication token.
|
||||
"""
|
||||
if not hasattr(self, "_auth"):
|
||||
self._authenticate()
|
||||
return self._auth
|
||||
|
||||
@auth.setter
|
||||
def auth(self, value):
|
||||
"""
|
||||
Sets any non-user authentication information associated with the
|
||||
request, such as an authentication token.
|
||||
"""
|
||||
self._auth = value
|
||||
|
||||
@property
|
||||
def successful_authenticator(self):
|
||||
"""
|
||||
Return the instance of the authentication instance class that was used
|
||||
to authenticate the request, or `None`.
|
||||
"""
|
||||
if not hasattr(self, "_authenticator"):
|
||||
self._authenticate()
|
||||
return self._authenticator
|
||||
|
||||
def _load_data_and_files(self):
|
||||
"""
|
||||
Parses the request content into self.DATA and self.FILES.
|
||||
"""
|
||||
if not _hasattr(self, "_content_type"):
|
||||
self._load_method_and_content_type()
|
||||
|
||||
if not _hasattr(self, "_data"):
|
||||
self._data, self._files = self._parse()
|
||||
|
||||
def _load_method_and_content_type(self):
|
||||
"""
|
||||
Sets the method and content_type, and then check if they"ve
|
||||
been overridden.
|
||||
"""
|
||||
self._content_type = self.META.get("HTTP_CONTENT_TYPE",
|
||||
self.META.get("CONTENT_TYPE", ""))
|
||||
|
||||
self._perform_form_overloading()
|
||||
|
||||
if not _hasattr(self, "_method"):
|
||||
self._method = self._request.method
|
||||
|
||||
# Allow X-HTTP-METHOD-OVERRIDE header
|
||||
self._method = self.META.get("HTTP_X_HTTP_METHOD_OVERRIDE",
|
||||
self._method)
|
||||
|
||||
def _load_stream(self):
|
||||
"""
|
||||
Return the content body of the request, as a stream.
|
||||
"""
|
||||
try:
|
||||
content_length = int(self.META.get("CONTENT_LENGTH",
|
||||
self.META.get("HTTP_CONTENT_LENGTH")))
|
||||
except (ValueError, TypeError):
|
||||
content_length = 0
|
||||
|
||||
if content_length == 0:
|
||||
self._stream = None
|
||||
elif hasattr(self._request, "read"):
|
||||
self._stream = self._request
|
||||
else:
|
||||
self._stream = BytesIO(self.raw_post_data)
|
||||
|
||||
def _perform_form_overloading(self):
|
||||
"""
|
||||
If this is a form POST request, then we need to check if the method and
|
||||
content/content_type have been overridden by setting them in hidden
|
||||
form fields or not.
|
||||
"""
|
||||
|
||||
USE_FORM_OVERLOADING = (
|
||||
self._METHOD_PARAM or
|
||||
(self._CONTENT_PARAM and self._CONTENTTYPE_PARAM)
|
||||
)
|
||||
|
||||
# We only need to use form overloading on form POST requests.
|
||||
if (not USE_FORM_OVERLOADING
|
||||
or self._request.method != "POST"
|
||||
or not is_form_media_type(self._content_type)):
|
||||
return
|
||||
|
||||
# At this point we"re committed to parsing the request as form data.
|
||||
self._data = self._request.POST
|
||||
self._files = self._request.FILES
|
||||
|
||||
# Method overloading - change the method and remove the param from the content.
|
||||
if (self._METHOD_PARAM and
|
||||
self._METHOD_PARAM in self._data):
|
||||
self._method = self._data[self._METHOD_PARAM].upper()
|
||||
|
||||
# Content overloading - modify the content type, and force re-parse.
|
||||
if (self._CONTENT_PARAM and
|
||||
self._CONTENTTYPE_PARAM and
|
||||
self._CONTENT_PARAM in self._data and
|
||||
self._CONTENTTYPE_PARAM in self._data):
|
||||
self._content_type = self._data[self._CONTENTTYPE_PARAM]
|
||||
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context["encoding"]))
|
||||
self._data, self._files = (Empty, Empty)
|
||||
|
||||
def _parse(self):
|
||||
"""
|
||||
Parse the request content, returning a two-tuple of (data, files)
|
||||
|
||||
May raise an `UnsupportedMediaType`, or `ParseError` exception.
|
||||
"""
|
||||
stream = self.stream
|
||||
media_type = self.content_type
|
||||
|
||||
if stream is None or media_type is None:
|
||||
empty_data = QueryDict("", self._request._encoding)
|
||||
empty_files = MultiValueDict()
|
||||
return (empty_data, empty_files)
|
||||
|
||||
parser = self.negotiator.select_parser(self, self.parsers)
|
||||
|
||||
if not parser:
|
||||
raise exceptions.UnsupportedMediaType(media_type)
|
||||
|
||||
try:
|
||||
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||
except:
|
||||
# If we get an exception during parsing, fill in empty data and
|
||||
# re-raise. Ensures we don't simply repeat the error when
|
||||
# attempting to render the browsable renderer response, or when
|
||||
# logging the request or similar.
|
||||
self._data = QueryDict("", self._request._encoding)
|
||||
self._files = MultiValueDict()
|
||||
raise
|
||||
|
||||
# Parser classes may return the raw data, or a
|
||||
# DataAndFiles object. Unpack the result as required.
|
||||
try:
|
||||
return (parsed.data, parsed.files)
|
||||
except AttributeError:
|
||||
empty_files = MultiValueDict()
|
||||
return (parsed, empty_files)
|
||||
|
||||
def _authenticate(self):
|
||||
"""
|
||||
Attempt to authenticate the request using each authentication instance
|
||||
in turn.
|
||||
Returns a three-tuple of (authenticator, user, authtoken).
|
||||
"""
|
||||
for authenticator in self.authenticators:
|
||||
try:
|
||||
user_auth_tuple = authenticator.authenticate(self)
|
||||
except exceptions.APIException:
|
||||
self._not_authenticated()
|
||||
raise
|
||||
|
||||
if not user_auth_tuple is None:
|
||||
self._authenticator = authenticator
|
||||
self._user, self._auth = user_auth_tuple
|
||||
return
|
||||
|
||||
self._not_authenticated()
|
||||
|
||||
def _not_authenticated(self):
|
||||
"""
|
||||
Return a three-tuple of (authenticator, user, authtoken), representing
|
||||
an unauthenticated request.
|
||||
|
||||
By default this will be (None, AnonymousUser, None).
|
||||
"""
|
||||
self._authenticator = None
|
||||
|
||||
if api_settings.UNAUTHENTICATED_USER:
|
||||
self._user = api_settings.UNAUTHENTICATED_USER()
|
||||
else:
|
||||
self._user = None
|
||||
|
||||
if api_settings.UNAUTHENTICATED_TOKEN:
|
||||
self._auth = api_settings.UNAUTHENTICATED_TOKEN()
|
||||
else:
|
||||
self._auth = None
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""
|
||||
Proxy other attributes to the underlying HttpRequest object.
|
||||
"""
|
||||
return getattr(self._request, attr)
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Provide reverse functions that return fully qualified URLs
|
||||
"""
|
||||
from django.core.urlresolvers import reverse as django_reverse
|
||||
from django.utils.functional import lazy
|
||||
|
||||
|
||||
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||
"""
|
||||
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
|
||||
and returns a fully qualified URL, using the request to get the base URL.
|
||||
"""
|
||||
if format is not None:
|
||||
kwargs = kwargs or {}
|
||||
kwargs["format"] = format
|
||||
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
|
||||
if request:
|
||||
return request.build_absolute_uri(url)
|
||||
return url
|
||||
|
||||
|
||||
reverse_lazy = lazy(reverse, str)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,226 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2015, Tom Christie
|
||||
|
||||
|
||||
"""
|
||||
Settings for REST framework are all namespaced in the REST_FRAMEWORK setting.
|
||||
For example your project's `settings.py` file might look like this:
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_RENDERER_CLASSES": (
|
||||
"taiga.base.api.renderers.JSONRenderer",
|
||||
)
|
||||
"DEFAULT_PARSER_CLASSES": (
|
||||
"taiga.base.api.parsers.JSONParser",
|
||||
)
|
||||
}
|
||||
|
||||
This module provides the `api_setting` object, that is used to access
|
||||
REST framework settings, checking for user settings first, then falling
|
||||
back to the defaults.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import importlib
|
||||
from django.utils import six
|
||||
|
||||
from . import ISO_8601
|
||||
|
||||
|
||||
USER_SETTINGS = getattr(settings, "REST_FRAMEWORK", None)
|
||||
|
||||
DEFAULTS = {
|
||||
# Base API policies
|
||||
"DEFAULT_RENDERER_CLASSES": (
|
||||
"taiga.base.api.renderers.JSONRenderer",
|
||||
"taiga.base.api.renderers.BrowsableAPIRenderer",
|
||||
),
|
||||
"DEFAULT_PARSER_CLASSES": (
|
||||
"taiga.base.api.parsers.JSONParser",
|
||||
"taiga.base.api.parsers.FormParser",
|
||||
"taiga.base.api.parsers.MultiPartParser"
|
||||
),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"taiga.base.api.authentication.SessionAuthentication",
|
||||
"taiga.base.api.authentication.BasicAuthentication"
|
||||
),
|
||||
"DEFAULT_PERMISSION_CLASSES": (
|
||||
"taiga.base.api.permissions.AllowAny",
|
||||
),
|
||||
"DEFAULT_THROTTLE_CLASSES": (
|
||||
),
|
||||
"DEFAULT_CONTENT_NEGOTIATION_CLASS":
|
||||
"taiga.base.api.negotiation.DefaultContentNegotiation",
|
||||
|
||||
# Genric view behavior
|
||||
"DEFAULT_MODEL_SERIALIZER_CLASS":
|
||||
"taiga.base.api.serializers.ModelSerializer",
|
||||
"DEFAULT_FILTER_BACKENDS": (),
|
||||
|
||||
# Throttling
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"user": None,
|
||||
"anon": None,
|
||||
},
|
||||
|
||||
# Pagination
|
||||
"PAGINATE_BY": None,
|
||||
"PAGINATE_BY_PARAM": None,
|
||||
"MAX_PAGINATE_BY": None,
|
||||
|
||||
# Authentication
|
||||
"UNAUTHENTICATED_USER": "django.contrib.auth.models.AnonymousUser",
|
||||
"UNAUTHENTICATED_TOKEN": None,
|
||||
|
||||
# View configuration
|
||||
"VIEW_NAME_FUNCTION": "taiga.base.api.views.get_view_name",
|
||||
"VIEW_DESCRIPTION_FUNCTION": "taiga.base.api.views.get_view_description",
|
||||
|
||||
# Exception handling
|
||||
"EXCEPTION_HANDLER": "taiga.base.api.views.exception_handler",
|
||||
|
||||
# Testing
|
||||
"TEST_REQUEST_RENDERER_CLASSES": (
|
||||
"taiga.base.api.renderers.MultiPartRenderer",
|
||||
"taiga.base.api.renderers.JSONRenderer"
|
||||
),
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "multipart",
|
||||
|
||||
# Browser enhancements
|
||||
"FORM_METHOD_OVERRIDE": "_method",
|
||||
"FORM_CONTENT_OVERRIDE": "_content",
|
||||
"FORM_CONTENTTYPE_OVERRIDE": "_content_type",
|
||||
"URL_ACCEPT_OVERRIDE": "accept",
|
||||
"URL_FORMAT_OVERRIDE": "format",
|
||||
|
||||
"FORMAT_SUFFIX_KWARG": "format",
|
||||
"URL_FIELD_NAME": "url",
|
||||
|
||||
# Input and output formats
|
||||
"DATE_INPUT_FORMATS": (
|
||||
ISO_8601,
|
||||
),
|
||||
"DATE_FORMAT": None,
|
||||
|
||||
"DATETIME_INPUT_FORMATS": (
|
||||
ISO_8601,
|
||||
),
|
||||
"DATETIME_FORMAT": None,
|
||||
|
||||
"TIME_INPUT_FORMATS": (
|
||||
ISO_8601,
|
||||
),
|
||||
"TIME_FORMAT": None,
|
||||
|
||||
# Pending deprecation
|
||||
"FILTER_BACKEND": None,
|
||||
}
|
||||
|
||||
|
||||
# List of settings that may be in string import notation.
|
||||
IMPORT_STRINGS = (
|
||||
"DEFAULT_RENDERER_CLASSES",
|
||||
"DEFAULT_PARSER_CLASSES",
|
||||
"DEFAULT_AUTHENTICATION_CLASSES",
|
||||
"DEFAULT_PERMISSION_CLASSES",
|
||||
"DEFAULT_THROTTLE_CLASSES",
|
||||
"DEFAULT_CONTENT_NEGOTIATION_CLASS",
|
||||
"DEFAULT_MODEL_SERIALIZER_CLASS",
|
||||
"DEFAULT_FILTER_BACKENDS",
|
||||
"EXCEPTION_HANDLER",
|
||||
"FILTER_BACKEND",
|
||||
"TEST_REQUEST_RENDERER_CLASSES",
|
||||
"UNAUTHENTICATED_USER",
|
||||
"UNAUTHENTICATED_TOKEN",
|
||||
"VIEW_NAME_FUNCTION",
|
||||
"VIEW_DESCRIPTION_FUNCTION"
|
||||
)
|
||||
|
||||
|
||||
def perform_import(val, setting_name):
|
||||
"""
|
||||
If the given setting is a string import notation,
|
||||
then perform the necessary import or imports.
|
||||
"""
|
||||
if isinstance(val, six.string_types):
|
||||
return import_from_string(val, setting_name)
|
||||
elif isinstance(val, (list, tuple)):
|
||||
return [import_from_string(item, setting_name) for item in val]
|
||||
return val
|
||||
|
||||
|
||||
def import_from_string(val, setting_name):
|
||||
"""
|
||||
Attempt to import a class from a string representation.
|
||||
"""
|
||||
try:
|
||||
# Nod to tastypie's use of importlib.
|
||||
parts = val.split('.')
|
||||
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
|
||||
module = importlib.import_module(module_path)
|
||||
return getattr(module, class_name)
|
||||
except ImportError as e:
|
||||
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
|
||||
raise ImportError(msg)
|
||||
|
||||
|
||||
class APISettings(object):
|
||||
"""
|
||||
A settings object, that allows API settings to be accessed as properties.
|
||||
For example:
|
||||
|
||||
from taiga.base.api.settings import api_settings
|
||||
print api_settings.DEFAULT_RENDERER_CLASSES
|
||||
|
||||
Any setting with string import paths will be automatically resolved
|
||||
and return the class, rather than the string literal.
|
||||
"""
|
||||
def __init__(self, user_settings=None, defaults=None, import_strings=None):
|
||||
self.user_settings = user_settings or {}
|
||||
self.defaults = defaults or {}
|
||||
self.import_strings = import_strings or ()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr not in self.defaults.keys():
|
||||
raise AttributeError("Invalid API setting: '%s'" % attr)
|
||||
|
||||
try:
|
||||
# Check if present in user settings
|
||||
val = self.user_settings[attr]
|
||||
except KeyError:
|
||||
# Fall back to defaults
|
||||
val = self.defaults[attr]
|
||||
|
||||
# Coerce import strings into classes
|
||||
if val and attr in self.import_strings:
|
||||
val = perform_import(val, attr)
|
||||
|
||||
self.validate_setting(attr, val)
|
||||
|
||||
# Cache the result
|
||||
setattr(self, attr, val)
|
||||
return val
|
||||
|
||||
def validate_setting(self, attr, val):
|
||||
if attr == "FILTER_BACKEND" and val is not None:
|
||||
# Make sure we can initialize the class
|
||||
val()
|
||||
|
||||
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is partially taken from django-rest-framework:
|
||||
* Copyright (c) 2011-2014, Tom Christie
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
This CSS file contains some tweaks specific to the included Bootstrap theme.
|
||||
It's separate from `style.css` so that it can be easily overridden by replacing
|
||||
a single block in the template.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
.form-actions {
|
||||
background: transparent;
|
||||
border-top-color: transparent;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.navbar-inverse .brand a {
|
||||
color: #999;
|
||||
}
|
||||
.navbar-inverse .brand:hover a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* custom navigation styles */
|
||||
.wrapper .navbar{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner{
|
||||
background: #2C2C2C;
|
||||
color: white;
|
||||
border: none;
|
||||
border-top: 5px solid #A30000;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-list > .active > a, .nav-list > .active > a:hover {
|
||||
background: #2c2c2c;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
||||
color: #A30000;
|
||||
}
|
||||
.navbar .navbar-inner .dropdown-menu li a:hover{
|
||||
background: #eeeeee;
|
||||
color: #c20000;
|
||||
}
|
||||
|
||||
/*=== dabapps bootstrap styles ====*/
|
||||
|
||||
html{
|
||||
width:100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
body, .navbar .navbar-inner .container-fluid {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body{
|
||||
background: url("../img/grid.png") repeat-x;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
#content{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* sticky footer and footer */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
|
||||
.form-switcher {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.well .form-actions {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well form .help-block {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.nav-tabs > li {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.nav-tabs li a {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nav-tabs > .active > a {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.nav-tabs > .active > a:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tabbable.first-tab-active .tab-content
|
||||
{
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
#footer, #push {
|
||||
height: 60px; /* .push must be the same height as .footer */
|
||||
}
|
||||
|
||||
#footer{
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
text-align: center;
|
||||
color: gray;
|
||||
border-top: 1px solid #DDD;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: gray;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#footer a:hover {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* custom general page styles */
|
||||
.hero-unit h2, .hero-unit h1{
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
body a, body a{
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
body a:hover{
|
||||
color: #c20000;
|
||||
}
|
||||
|
||||
#content a span{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.request-info {
|
||||
clear:both;
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is partially taken from django-rest-framework:
|
||||
* Copyright (c) 2011-2014, Tom Christie
|
||||
*/
|
||||
|
||||
/* The navbar is fixed at >= 980px wide, so add padding to the body to prevent
|
||||
content running up underneath it. */
|
||||
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.resource-description, .response-info {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.version:before {
|
||||
content: "v";
|
||||
opacity: 0.6;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
.format-option {
|
||||
font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
|
||||
}
|
||||
|
||||
.button-form {
|
||||
float: right;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
ul.breadcrumb {
|
||||
margin: 58px 0 0 0;
|
||||
}
|
||||
|
||||
form select, form input, form textarea {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
form select[multiple] {
|
||||
height: 150px;
|
||||
}
|
||||
/* To allow tooltips to work on disabled elements */
|
||||
.disabled-tooltip-shield {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.errorlist {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
word-wrap: normal;
|
||||
white-space: pre;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is partially taken from django-rest-framework:
|
||||
* Copyright (c) 2011-2014, Tom Christie
|
||||
*/
|
||||
.com { color: #93a1a1; }
|
||||
.lit { color: #195f91; }
|
||||
.pun, .opn, .clo { color: #93a1a1; }
|
||||
.fun { color: #dc322f; }
|
||||
.str, .atv { color: #D14; }
|
||||
.kwd, .prettyprint .tag { color: #1e347b; }
|
||||
.typ, .atn, .dec, .var { color: teal; }
|
||||
.pln { color: #48484c; }
|
||||
|
||||
.prettyprint {
|
||||
padding: 8px;
|
||||
background-color: #f7f7f9;
|
||||
border: 1px solid #e1e1e8;
|
||||
}
|
||||
.prettyprint.linenums {
|
||||
-webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
|
||||
-moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
|
||||
box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
|
||||
}
|
||||
|
||||
/* Specify class=linenums on a pre to get line numbering */
|
||||
ol.linenums {
|
||||
margin: 0 0 0 33px; /* IE indents via margin-left */
|
||||
}
|
||||
ol.linenums li {
|
||||
padding-left: 12px;
|
||||
color: #bebec5;
|
||||
line-height: 20px;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is partially taken from django-rest-framework:
|
||||
* Copyright (c) 2011-2014, Tom Christie
|
||||
*/
|
||||
|
||||
|
||||
function getCookie(c_name)
|
||||
{
|
||||
// From http://www.w3schools.com/js/js_cookies.asp
|
||||
var c_value = document.cookie;
|
||||
var c_start = c_value.indexOf(" " + c_name + "=");
|
||||
if (c_start == -1) {
|
||||
c_start = c_value.indexOf(c_name + "=");
|
||||
}
|
||||
if (c_start == -1) {
|
||||
c_value = null;
|
||||
} else {
|
||||
c_start = c_value.indexOf("=", c_start) + 1;
|
||||
var c_end = c_value.indexOf(";", c_start);
|
||||
if (c_end == -1) {
|
||||
c_end = c_value.length;
|
||||
}
|
||||
c_value = unescape(c_value.substring(c_start,c_end));
|
||||
}
|
||||
return c_value;
|
||||
}
|
||||
|
||||
// JSON highlighting.
|
||||
prettyPrint();
|
||||
|
||||
// Bootstrap tooltips.
|
||||
$('.js-tooltip').tooltip({
|
||||
delay: 1000
|
||||
});
|
||||
|
||||
// Deal with rounded tab styling after tab clicks.
|
||||
$('a[data-toggle="tab"]:first').on('shown', function (e) {
|
||||
$(e.target).parents('.tabbable').addClass('first-tab-active');
|
||||
});
|
||||
$('a[data-toggle="tab"]:not(:first)').on('shown', function (e) {
|
||||
$(e.target).parents('.tabbable').removeClass('first-tab-active');
|
||||
});
|
||||
|
||||
$('a[data-toggle="tab"]').click(function(){
|
||||
document.cookie="tabstyle=" + this.name + "; path=/";
|
||||
});
|
||||
|
||||
// Store tab preference in cookies & display appropriate tab on load.
|
||||
var selectedTab = null;
|
||||
var selectedTabName = getCookie('tabstyle');
|
||||
|
||||
if (selectedTabName) {
|
||||
selectedTab = $('.form-switcher a[name=' + selectedTabName + ']');
|
||||
}
|
||||
|
||||
if (selectedTab && selectedTab.length > 0) {
|
||||
// Display whichever tab is selected.
|
||||
selectedTab.tab('show');
|
||||
} else {
|
||||
// If no tab selected, display rightmost tab.
|
||||
$('.form-switcher a:first').tab('show');
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
* Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
* Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is partially taken from django-rest-framework:
|
||||
* Copyright (c) 2011-2014, Tom Christie
|
||||
*/
|
||||
var q=null;window.PR_SHOULD_USE_CONTINUATION=!0;
|
||||
(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a=
|
||||
[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c<i;++c){var j=f[c];if(/\\[bdsw]/i.test(j))a.push(j);else{var j=m(j),d;c+2<i&&"-"===f[c+1]?(d=m(f[c+2]),c+=2):d=j;b.push([j,d]);d<65||j>122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;c<b.length;++c)i=b[c],i[0]<=j[1]+1?j[1]=Math.max(j[1],i[1]):f.push(j=i);b=["["];o&&b.push("^");b.push.apply(b,a);for(c=0;c<
|
||||
f.length;++c)i=f[c],b.push(e(i[0])),i[1]>i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c<b;++c){var j=f[c];j==="("?++i:"\\"===j.charAt(0)&&(j=+j.substring(1))&&j<=i&&(d[j]=-1)}for(c=1;c<d.length;++c)-1===d[c]&&(d[c]=++t);for(i=c=0;c<b;++c)j=f[c],j==="("?(++i,d[i]===void 0&&(f[c]="(?:")):"\\"===j.charAt(0)&&
|
||||
(j=+j.substring(1))&&j<=i&&(f[c]="\\"+d[i]);for(i=c=0;c<b;++c)"^"===f[c]&&"^"!==f[c+1]&&(f[c]="");if(a.ignoreCase&&s)for(c=0;c<b;++c)j=f[c],a=j.charAt(0),j.length>=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p<d;++p){var g=a[p];if(g.ignoreCase)l=!0;else if(/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){s=!0;l=!1;break}}for(var r=
|
||||
{b:8,t:9,n:10,v:11,f:12,r:13},n=[],p=0,d=a.length;p<d;++p){g=a[p];if(g.global||g.multiline)throw Error(""+g);n.push("(?:"+y(g)+")")}return RegExp(n.join("|"),l?"gi":"g")}function M(a){function m(a){switch(a.nodeType){case 1:if(e.test(a.className))break;for(var g=a.firstChild;g;g=g.nextSibling)m(g);g=a.nodeName;if("BR"===g||"LI"===g)h[s]="\n",t[s<<1]=y++,t[s++<<1|1]=a;break;case 3:case 4:g=a.nodeValue,g.length&&(g=p?g.replace(/\r\n?/g,"\n"):g.replace(/[\t\n\r ]+/g," "),h[s]=g,t[s<<1]=y,y+=g.length,
|
||||
t[s++<<1|1]=a)}}var e=/(?:^|\s)nocode(?:\s|$)/,h=[],y=0,t=[],s=0,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=document.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);m(a);return{a:h.join("").replace(/\n$/,""),c:t}}function B(a,m,e,h){m&&(a={a:m,d:a},e(a),h.push.apply(h,a.e))}function x(a,m){function e(a){for(var l=a.d,p=[l,"pln"],d=0,g=a.a.match(y)||[],r={},n=0,z=g.length;n<z;++n){var f=g[n],b=r[f],o=void 0,c;if(typeof b===
|
||||
"string")c=!1;else{var i=h[f.charAt(0)];if(i)o=f.match(i[1]),b=i[0];else{for(c=0;c<t;++c)if(i=m[c],o=f.match(i[1])){b=i[0];break}o||(b="pln")}if((c=b.length>=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m),
|
||||
l=[],p={},d=0,g=e.length;d<g;++d){var r=e[d],n=r[3];if(n)for(var k=n.length;--k>=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,
|
||||
q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/,
|
||||
q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,
|
||||
"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),
|
||||
a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}
|
||||
for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g<d.length;++g)e(d[g]);m===(m|0)&&d[0].setAttribute("value",
|
||||
m);var r=s.createElement("OL");r.className="linenums";for(var n=Math.max(0,m-1|0)||0,g=0,z=d.length;g<z;++g)l=d[g],l.className="L"+(g+n)%10,l.firstChild||l.appendChild(s.createTextNode("\xa0")),r.appendChild(l);a.appendChild(r)}function k(a,m){for(var e=m.length;--e>=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(m)?"default-markup":"default-code";return A[a]}function E(a){var m=
|
||||
a.g;try{var e=M(a.h),h=e.a;a.a=h;a.c=e.c;a.d=0;C(m,h)(a);var k=/\bMSIE\b/.test(navigator.userAgent),m=/\n/g,t=a.a,s=t.length,e=0,l=a.c,p=l.length,h=0,d=a.e,g=d.length,a=0;d[g]=s;var r,n;for(n=r=0;n<g;)d[n]!==d[n+2]?(d[r++]=d[n++],d[r++]=d[n++]):n+=2;g=r;for(n=r=0;n<g;){for(var z=d[n],f=d[n+1],b=n+2;b+2<=g&&d[b+1]===f;)b+=2;d[r++]=z;d[r++]=f;n=b}for(d.length=r;h<p;){var o=l[h+2]||s,c=d[a+2]||s,b=Math.min(o,c),i=l[h+1],j;if(i.nodeType!==1&&(j=t.substring(e,b))){k&&(j=j.replace(m,"\r"));i.nodeValue=
|
||||
j;var u=i.ownerDocument,v=u.createElement("SPAN");v.className=d[a+1];var x=i.parentNode;x.replaceChild(v,i);v.appendChild(i);e<o&&(l[h+1]=i=u.createTextNode(t.substring(b,o)),x.insertBefore(i,v.nextSibling))}e=b;e>=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],
|
||||
"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],
|
||||
H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],
|
||||
J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+
|
||||
I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),
|
||||
["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",
|
||||
/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),
|
||||
["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",
|
||||
hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p<h.length&&l.now()<e;p++){var n=h[p],k=n.className;if(k.indexOf("prettyprint")>=0){var k=k.match(g),f,b;if(b=
|
||||
!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p<h.length?setTimeout(m,
|
||||
250):a&&a()}for(var e=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],h=[],k=0;k<e.length;++k)for(var t=0,s=e[k].length;t<s;++t)h.push(e[k][t]);var e=q,l=Date;l.now||(l={now:function(){return+new Date}});var p=0,d,g=/\blang(?:uage)?-([\w.]+)(?!\S)/;m()};window.PR={createSimpleLexer:x,registerLangHandler:k,sourceDecorator:u,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",
|
||||
PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ"}})();
|
|
@ -0,0 +1,3 @@
|
|||
{% extends "api/base.html" %}
|
||||
|
||||
{# Override this template in your own templates directory to customize #}
|
|
@ -0,0 +1,237 @@
|
|||
{% load url from future %}
|
||||
{% load api %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
|
||||
{% block meta %}
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
{% endblock %}
|
||||
|
||||
<title>{% block title %}Taiga API REST{% endblock %}</title>
|
||||
|
||||
{% block style %}
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap.min.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap-tweaks.css" %}"/>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/prettify.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/default.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="{% block bodyclass %}{% endblock %} container">
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
{% block navbar %}
|
||||
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<span href="/">
|
||||
{% block branding %}
|
||||
<a class='brand' rel="nofollow" href='https://taiga.io'>
|
||||
Taiga API REST
|
||||
</a>
|
||||
{% endblock %}
|
||||
</span>
|
||||
<ul class="nav pull-right">
|
||||
{% block userlinks %}
|
||||
{% if user.is_authenticated %}
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
{{ user }}
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>{% optional_logout request %}</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>{% optional_login request %}</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<ul class="breadcrumb">
|
||||
{% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
|
||||
<li>
|
||||
<a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">›</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Content -->
|
||||
<div id="content">
|
||||
|
||||
{% if 'GET' in allowed_methods %}
|
||||
<form id="get-form" class="pull-right">
|
||||
<fieldset>
|
||||
<div class="btn-group format-selection">
|
||||
<a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
|
||||
|
||||
<button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for format in available_formats %}
|
||||
<li>
|
||||
<a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if options_form %}
|
||||
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
|
||||
<button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if delete_form %}
|
||||
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
||||
<button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="content-main">
|
||||
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||
{% block description %}
|
||||
{{ description }}
|
||||
{% endblock %}
|
||||
<div class="request-info" style="clear: both" >
|
||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||
</div>
|
||||
<div class="response-info">
|
||||
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
||||
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
||||
{% endfor %}
|
||||
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if display_edit_forms %}
|
||||
|
||||
{% if post_form or raw_data_post_form %}
|
||||
<div {% if post_form %}class="tabbable"{% endif %}>
|
||||
{% if post_form %}
|
||||
<ul class="nav nav-tabs form-switcher">
|
||||
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="well tab-content">
|
||||
{% if post_form %}
|
||||
<div class="tab-pane" id="object-form">
|
||||
{% with form=post_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||
<fieldset>
|
||||
{{ post_form }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form">
|
||||
{% with form=raw_data_post_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||
<fieldset>
|
||||
{% include "api/raw_data_form.html" %}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if put_form or raw_data_put_form or raw_data_patch_form %}
|
||||
<div {% if put_form %}class="tabbable"{% endif %}>
|
||||
{% if put_form %}
|
||||
<ul class="nav nav-tabs form-switcher">
|
||||
<li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
|
||||
<li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="well tab-content">
|
||||
{% if put_form %}
|
||||
<div class="tab-pane" id="object-form">
|
||||
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
|
||||
<fieldset>
|
||||
{{ put_form }}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
|
||||
{% with form=raw_data_put_or_patch_form %}
|
||||
<form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
|
||||
<fieldset>
|
||||
{% include "api/raw_data_form.html" %}
|
||||
<div class="form-actions">
|
||||
{% if raw_data_put_form %}
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
|
||||
{% endif %}
|
||||
{% if raw_data_patch_form %}
|
||||
<button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<!-- END content-main -->
|
||||
|
||||
</div>
|
||||
<!-- END Content -->
|
||||
|
||||
<div id="push"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div><!-- ./wrapper -->
|
||||
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script src="{% static "api/js/jquery-1.8.1-min.js" %}"></script>
|
||||
<script src="{% static "api/js/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "api/js/prettify-min.js" %}"></script>
|
||||
<script src="{% static "api/js/default.js" %}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
{% load api %}
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{% for field in form.fields.values %}
|
||||
{% if not field.read_only %}
|
||||
<div class="control-group {% if field.errors %}error{% endif %}">
|
||||
{{ field.label_tag|add_class:"control-label" }}
|
||||
<div class="controls">
|
||||
{{ field.widget_html }}
|
||||
{% if field.help_text %}<span class="help-block">{{ field.help_text }}</span>{% endif %}
|
||||
{% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -0,0 +1,3 @@
|
|||
{% extends "api/login_base.html" %}
|
||||
|
||||
{# Override this template in your own templates directory to customize #}
|
|
@ -0,0 +1,53 @@
|
|||
{% load url from future %}
|
||||
{% load api %}
|
||||
<html>
|
||||
|
||||
<head>
|
||||
{% block style %}
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap.min.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/bootstrap-tweaks.css" %}"/>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "api/css/default.css" %}"/>
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="container">
|
||||
|
||||
<div class="container-fluid" style="margin-top: 30px">
|
||||
<div class="row-fluid">
|
||||
<div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
|
||||
<div class="row-fluid">
|
||||
<div>
|
||||
{% block branding %}<h3 style="margin: 0 0 20px;">Taiga API REST</h3>{% endblock %}
|
||||
</div>
|
||||
</div><!-- /row fluid -->
|
||||
|
||||
<div class="row-fluid">
|
||||
<div>
|
||||
<form action="{% url 'api:login' %}" class=" form-inline" method="post">
|
||||
{% csrf_token %}
|
||||
<div id="div_id_username" class="clearfix control-group">
|
||||
<div class="controls">
|
||||
<Label class="span4">Username:</label>
|
||||
<input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
|
||||
</div>
|
||||
</div>
|
||||
<div id="div_id_password" class="clearfix control-group">
|
||||
<div class="controls">
|
||||
<Label class="span4">Password:</label>
|
||||
<input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
<div class="form-actions-no-box">
|
||||
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div><!-- /.row-fluid -->
|
||||
</div><!--/.well-->
|
||||
</div><!-- /.row-fluid -->
|
||||
</div><!-- /.container-fluid -->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,12 @@
|
|||
{% load api %}
|
||||
{% csrf_token %}
|
||||
{{ form.non_field_errors }}
|
||||
{% for field in form %}
|
||||
<div class="control-group">
|
||||
{{ field.label_tag|add_class:"control-label" }}
|
||||
<div class="controls">
|
||||
{{ field }}
|
||||
<span class="help-block">{{ field.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -0,0 +1,233 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
from django import template
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
|
||||
from django.http import QueryDict
|
||||
from django.utils.encoding import iri_to_uri
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeData, mark_safe
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import smart_urlquote
|
||||
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
|
||||
@register.tag("static")
|
||||
def do_static(parser, token):
|
||||
return StaticFilesNode.handle_token(parser, token)
|
||||
|
||||
|
||||
def replace_query_param(url, key, val):
|
||||
"""
|
||||
Given a URL and a key/val pair, set or replace an item in the query
|
||||
parameters of the URL, and return the new URL.
|
||||
"""
|
||||
(scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
|
||||
query_dict = QueryDict(query).copy()
|
||||
query_dict[key] = val
|
||||
query = query_dict.urlencode()
|
||||
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||
|
||||
|
||||
# Regex for adding classes to html snippets
|
||||
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
|
||||
|
||||
|
||||
# And the template tags themselves...
|
||||
|
||||
@register.simple_tag
|
||||
def optional_login(request):
|
||||
"""
|
||||
Include a login snippet if REST framework's login view is in the URLconf.
|
||||
"""
|
||||
try:
|
||||
login_url = reverse("api:login")
|
||||
except NoReverseMatch:
|
||||
return ""
|
||||
|
||||
snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
|
||||
return snippet
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def optional_logout(request):
|
||||
"""
|
||||
Include a logout snippet if REST framework's logout view is in the URLconf.
|
||||
"""
|
||||
try:
|
||||
logout_url = reverse("api:logout")
|
||||
except NoReverseMatch:
|
||||
return ""
|
||||
|
||||
snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
|
||||
return snippet
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def add_query_param(request, key, val):
|
||||
"""
|
||||
Add a query parameter to the current request url, and return the new url.
|
||||
"""
|
||||
iri = request.get_full_path()
|
||||
uri = iri_to_uri(iri)
|
||||
return replace_query_param(uri, key, val)
|
||||
|
||||
|
||||
@register.filter
|
||||
def add_class(value, css_class):
|
||||
"""
|
||||
http://stackoverflow.com/questions/4124220/django-adding-css-classes-when-rendering-form-fields-in-a-template
|
||||
|
||||
Inserts classes into template variables that contain HTML tags,
|
||||
useful for modifying forms without needing to change the Form objects.
|
||||
|
||||
Usage:
|
||||
|
||||
{{ field.label_tag|add_class:"control-label" }}
|
||||
|
||||
In the case of REST Framework, the filter is used to add Bootstrap-specific
|
||||
classes to the forms.
|
||||
"""
|
||||
html = six.text_type(value)
|
||||
match = class_re.search(html)
|
||||
if match:
|
||||
m = re.search(r"^%s$|^%s\s|\s%s\s|\s%s$" % (css_class, css_class,
|
||||
css_class, css_class),
|
||||
match.group(1))
|
||||
if not m:
|
||||
return mark_safe(class_re.sub(match.group(1) + " " + css_class,
|
||||
html))
|
||||
else:
|
||||
return mark_safe(html.replace(">", ' class="%s">' % css_class, 1))
|
||||
return value
|
||||
|
||||
|
||||
# Bunch of stuff cloned from urlize
|
||||
TRAILING_PUNCTUATION = [".", ",", ":", ";", ".)", "\"", "'"]
|
||||
WRAPPING_PUNCTUATION = [("(", ")"), ("<", ">"), ("[", "]"), ("<", ">"),
|
||||
("\"", "\""), ("'", "'")]
|
||||
word_split_re = re.compile(r"(\s+)")
|
||||
simple_url_re = re.compile(r"^https?://\[?\w", re.IGNORECASE)
|
||||
simple_url_2_re = re.compile(r"^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$", re.IGNORECASE)
|
||||
simple_email_re = re.compile(r"^\S+@\S+\.\S+$")
|
||||
|
||||
|
||||
def smart_urlquote_wrapper(matched_url):
|
||||
"""
|
||||
Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can
|
||||
be raised here, see issue #1386
|
||||
"""
|
||||
try:
|
||||
return smart_urlquote(matched_url)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@register.filter
|
||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
|
||||
"""
|
||||
Converts any URLs in text into clickable links.
|
||||
|
||||
Works on http://, https://, www. links, and also on links ending in one of
|
||||
the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org).
|
||||
Links can have trailing punctuation (periods, commas, close-parens) and
|
||||
leading punctuation (opening parens) and it"ll still do the right thing.
|
||||
|
||||
If trim_url_limit is not None, the URLs in link text longer than this limit
|
||||
will truncated to trim_url_limit-3 characters and appended with an elipsis.
|
||||
|
||||
If nofollow is True, the URLs in link text will get a rel="nofollow"
|
||||
attribute.
|
||||
|
||||
If autoescape is True, the link text and URLs will get autoescaped.
|
||||
"""
|
||||
trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ("%s..." % x[:max(0, limit - 3)])) or x
|
||||
safe_input = isinstance(text, SafeData)
|
||||
words = word_split_re.split(force_text(text))
|
||||
for i, word in enumerate(words):
|
||||
if "." in word or "@" in word or ":" in word:
|
||||
# Deal with punctuation.
|
||||
lead, middle, trail = "", word, ""
|
||||
for punctuation in TRAILING_PUNCTUATION:
|
||||
if middle.endswith(punctuation):
|
||||
middle = middle[:-len(punctuation)]
|
||||
trail = punctuation + trail
|
||||
for opening, closing in WRAPPING_PUNCTUATION:
|
||||
if middle.startswith(opening):
|
||||
middle = middle[len(opening):]
|
||||
lead = lead + opening
|
||||
# Keep parentheses at the end only if they"re balanced.
|
||||
if (middle.endswith(closing)
|
||||
and middle.count(closing) == middle.count(opening) + 1):
|
||||
middle = middle[:-len(closing)]
|
||||
trail = closing + trail
|
||||
|
||||
# Make URL we want to point to.
|
||||
url = None
|
||||
nofollow_attr = ' rel="nofollow"' if nofollow else ""
|
||||
if simple_url_re.match(middle):
|
||||
url = smart_urlquote_wrapper(middle)
|
||||
elif simple_url_2_re.match(middle):
|
||||
url = smart_urlquote_wrapper("http://%s" % middle)
|
||||
elif not ":" in middle and simple_email_re.match(middle):
|
||||
local, domain = middle.rsplit("@", 1)
|
||||
try:
|
||||
domain = domain.encode("idna").decode("ascii")
|
||||
except UnicodeError:
|
||||
continue
|
||||
url = "mailto:%s@%s" % (local, domain)
|
||||
nofollow_attr = ""
|
||||
|
||||
# Make link.
|
||||
if url:
|
||||
trimmed = trim_url(middle)
|
||||
if autoescape and not safe_input:
|
||||
lead, trail = escape(lead), escape(trail)
|
||||
url, trimmed = escape(url), escape(trimmed)
|
||||
middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
|
||||
words[i] = mark_safe("%s%s%s" % (lead, middle, trail))
|
||||
else:
|
||||
if safe_input:
|
||||
words[i] = mark_safe(word)
|
||||
elif autoescape:
|
||||
words[i] = escape(word)
|
||||
elif safe_input:
|
||||
words[i] = mark_safe(word)
|
||||
elif autoescape:
|
||||
words[i] = escape(word)
|
||||
return "".join(words)
|
||||
|
||||
|
||||
@register.filter
|
||||
def break_long_headers(header):
|
||||
"""
|
||||
Breaks headers longer than 160 characters (~page length)
|
||||
when possible (are comma separated)
|
||||
"""
|
||||
if len(header) > 160 and "," in header:
|
||||
header = mark_safe("<br> " + ", <br>".join(header.split(",")))
|
||||
return header
|
|
@ -0,0 +1,255 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Provides various throttling policies.
|
||||
"""
|
||||
|
||||
from django.core.cache import cache as default_cache
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .settings import api_settings
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class BaseThrottle(object):
|
||||
"""
|
||||
Rate throttling of requests.
|
||||
"""
|
||||
def allow_request(self, request, view):
|
||||
"""
|
||||
Return `True` if the request should be allowed, `False` otherwise.
|
||||
"""
|
||||
raise NotImplementedError(".allow_request() must be overridden")
|
||||
|
||||
def wait(self):
|
||||
"""
|
||||
Optionally, return a recommended number of seconds to wait before
|
||||
the next request.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class SimpleRateThrottle(BaseThrottle):
|
||||
"""
|
||||
A simple cache implementation, that only requires `.get_cache_key()`
|
||||
to be overridden.
|
||||
|
||||
The rate (requests / seconds) is set by a `throttle` attribute on the View
|
||||
class. The attribute is a string of the form "number_of_requests/period".
|
||||
|
||||
Period should be one of: ("s", "sec", "m", "min", "h", "hour", "d", "day")
|
||||
|
||||
Previous request information used for throttling is stored in the cache.
|
||||
"""
|
||||
|
||||
cache = default_cache
|
||||
timer = time.time
|
||||
cache_format = "throtte_%(scope)s_%(ident)s"
|
||||
scope = None
|
||||
THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
|
||||
|
||||
def __init__(self):
|
||||
if not getattr(self, "rate", None):
|
||||
self.rate = self.get_rate()
|
||||
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
"""
|
||||
Should return a unique cache-key which can be used for throttling.
|
||||
Must be overridden.
|
||||
|
||||
May return `None` if the request should not be throttled.
|
||||
"""
|
||||
raise NotImplementedError(".get_cache_key() must be overridden")
|
||||
|
||||
def get_rate(self):
|
||||
"""
|
||||
Determine the string representation of the allowed request rate.
|
||||
"""
|
||||
if not getattr(self, "scope", None):
|
||||
msg = ("You must set either `.scope` or `.rate` for \"%s\" throttle" %
|
||||
self.__class__.__name__)
|
||||
raise ImproperlyConfigured(msg)
|
||||
|
||||
try:
|
||||
return self.THROTTLE_RATES[self.scope]
|
||||
except KeyError:
|
||||
msg = "No default throttle rate set for \"%s\" scope" % self.scope
|
||||
raise ImproperlyConfigured(msg)
|
||||
|
||||
def parse_rate(self, rate):
|
||||
"""
|
||||
Given the request rate string, return a two tuple of:
|
||||
<allowed number of requests>, <period of time in seconds>
|
||||
"""
|
||||
if rate is None:
|
||||
return (None, None)
|
||||
num, period = rate.split("/")
|
||||
num_requests = int(num)
|
||||
duration = {"s": 1, "m": 60, "h": 3600, "d": 86400}[period[0]]
|
||||
return (num_requests, duration)
|
||||
|
||||
def allow_request(self, request, view):
|
||||
"""
|
||||
Implement the check to see if the request should be throttled.
|
||||
|
||||
On success calls `throttle_success`.
|
||||
On failure calls `throttle_failure`.
|
||||
"""
|
||||
if self.rate is None:
|
||||
return True
|
||||
|
||||
self.key = self.get_cache_key(request, view)
|
||||
if self.key is None:
|
||||
return True
|
||||
|
||||
self.history = self.cache.get(self.key, [])
|
||||
self.now = self.timer()
|
||||
|
||||
# Drop any requests from the history which have now passed the
|
||||
# throttle duration
|
||||
while self.history and self.history[-1] <= self.now - self.duration:
|
||||
self.history.pop()
|
||||
if len(self.history) >= self.num_requests:
|
||||
return self.throttle_failure()
|
||||
return self.throttle_success()
|
||||
|
||||
def throttle_success(self):
|
||||
"""
|
||||
Inserts the current request's timestamp along with the key
|
||||
into the cache.
|
||||
"""
|
||||
self.history.insert(0, self.now)
|
||||
self.cache.set(self.key, self.history, self.duration)
|
||||
return True
|
||||
|
||||
def throttle_failure(self):
|
||||
"""
|
||||
Called when a request to the API has failed due to throttling.
|
||||
"""
|
||||
return False
|
||||
|
||||
def wait(self):
|
||||
"""
|
||||
Returns the recommended next request time in seconds.
|
||||
"""
|
||||
if self.history:
|
||||
remaining_duration = self.duration - (self.now - self.history[-1])
|
||||
else:
|
||||
remaining_duration = self.duration
|
||||
|
||||
available_requests = self.num_requests - len(self.history) + 1
|
||||
if available_requests <= 0:
|
||||
return None
|
||||
|
||||
return remaining_duration / float(available_requests)
|
||||
|
||||
|
||||
class AnonRateThrottle(SimpleRateThrottle):
|
||||
"""
|
||||
Limits the rate of API calls that may be made by a anonymous users.
|
||||
|
||||
The IP address of the request will be used as the unique cache key.
|
||||
"""
|
||||
scope = "anon"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
if request.user.is_authenticated():
|
||||
return None # Only throttle unauthenticated requests.
|
||||
|
||||
ident = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
if ident is None:
|
||||
ident = request.META.get("REMOTE_ADDR")
|
||||
|
||||
return self.cache_format % {
|
||||
"scope": self.scope,
|
||||
"ident": ident
|
||||
}
|
||||
|
||||
|
||||
class UserRateThrottle(SimpleRateThrottle):
|
||||
"""
|
||||
Limits the rate of API calls that may be made by a given user.
|
||||
|
||||
The user id will be used as a unique cache key if the user is
|
||||
authenticated. For anonymous requests, the IP address of the request will
|
||||
be used.
|
||||
"""
|
||||
scope = "user"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
if request.user.is_authenticated():
|
||||
ident = request.user.id
|
||||
else:
|
||||
ident = request.META.get("REMOTE_ADDR", None)
|
||||
|
||||
return self.cache_format % {
|
||||
"scope": self.scope,
|
||||
"ident": ident
|
||||
}
|
||||
|
||||
|
||||
class ScopedRateThrottle(SimpleRateThrottle):
|
||||
"""
|
||||
Limits the rate of API calls by different amounts for various parts of
|
||||
the API. Any view that has the `throttle_scope` property set will be
|
||||
throttled. The unique cache key will be generated by concatenating the
|
||||
user id of the request, and the scope of the view being accessed.
|
||||
"""
|
||||
scope_attr = "throttle_scope"
|
||||
|
||||
def __init__(self):
|
||||
# Override the usual SimpleRateThrottle, because we can't determine
|
||||
# the rate until called by the view.
|
||||
pass
|
||||
|
||||
def allow_request(self, request, view):
|
||||
# We can only determine the scope once we"re called by the view.
|
||||
self.scope = getattr(view, self.scope_attr, None)
|
||||
|
||||
# If a view does not have a `throttle_scope` always allow the request
|
||||
if not self.scope:
|
||||
return True
|
||||
|
||||
# Determine the allowed request rate as we normally would during
|
||||
# the `__init__` call.
|
||||
self.rate = self.get_rate()
|
||||
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||
|
||||
# We can now proceed as normal.
|
||||
return super(ScopedRateThrottle, self).allow_request(request, view)
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
"""
|
||||
If `view.throttle_scope` is not set, don't apply this throttle.
|
||||
|
||||
Otherwise generate the unique cache key by concatenating the user id
|
||||
with the ".throttle_scope` property of the view.
|
||||
"""
|
||||
if request.user.is_authenticated():
|
||||
ident = request.user.id
|
||||
else:
|
||||
ident = request.META.get("REMOTE_ADDR", None)
|
||||
|
||||
return self.cache_format % {
|
||||
"scope": self.scope,
|
||||
"ident": ident
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
|
||||
from django.core.urlresolvers import RegexURLResolver
|
||||
from django.conf.urls import patterns, url, include
|
||||
|
||||
from .settings import api_settings
|
||||
|
||||
|
||||
def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required):
|
||||
ret = []
|
||||
for urlpattern in urlpatterns:
|
||||
if isinstance(urlpattern, RegexURLResolver):
|
||||
# Set of included URL patterns
|
||||
regex = urlpattern.regex.pattern
|
||||
namespace = urlpattern.namespace
|
||||
app_name = urlpattern.app_name
|
||||
kwargs = urlpattern.default_kwargs
|
||||
# Add in the included patterns, after applying the suffixes
|
||||
patterns = apply_suffix_patterns(urlpattern.url_patterns,
|
||||
suffix_pattern,
|
||||
suffix_required)
|
||||
ret.append(url(regex, include(patterns, namespace, app_name), kwargs))
|
||||
|
||||
else:
|
||||
# Regular URL pattern
|
||||
regex = urlpattern.regex.pattern.rstrip("$") + suffix_pattern
|
||||
view = urlpattern._callback or urlpattern._callback_str
|
||||
kwargs = urlpattern.default_args
|
||||
name = urlpattern.name
|
||||
# Add in both the existing and the new urlpattern
|
||||
if not suffix_required:
|
||||
ret.append(urlpattern)
|
||||
ret.append(url(regex, view, kwargs, name))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
|
||||
"""
|
||||
Supplement existing urlpatterns with corresponding patterns that also
|
||||
include a ".format" suffix. Retains urlpattern ordering.
|
||||
|
||||
urlpatterns:
|
||||
A list of URL patterns.
|
||||
|
||||
suffix_required:
|
||||
If `True`, only suffixed URLs will be generated, and non-suffixed
|
||||
URLs will not be used. Defaults to `False`.
|
||||
|
||||
allowed:
|
||||
An optional tuple/list of allowed suffixes. eg ["json", "api"]
|
||||
Defaults to `None`, which allows any suffix.
|
||||
"""
|
||||
suffix_kwarg = api_settings.FORMAT_SUFFIX_KWARG
|
||||
if allowed:
|
||||
if len(allowed) == 1:
|
||||
allowed_pattern = allowed[0]
|
||||
else:
|
||||
allowed_pattern = "(%s)" % "|".join(allowed)
|
||||
suffix_pattern = r"\.(?P<%s>%s)$" % (suffix_kwarg, allowed_pattern)
|
||||
else:
|
||||
suffix_pattern = r"\.(?P<%s>[a-z0-9]+)$" % suffix_kwarg
|
||||
|
||||
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
|
|
@ -0,0 +1,43 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Login and logout views for the browsable API.
|
||||
|
||||
Add these to your root URLconf if you're using the browsable API and
|
||||
your API requires authentication.
|
||||
|
||||
The urls must be namespaced as 'api', and you should make sure
|
||||
your authentication settings include `SessionAuthentication`.
|
||||
|
||||
urlpatterns = patterns('',
|
||||
...
|
||||
url(r'^auth', include('taiga.base.api.urls', namespace='api'))
|
||||
)
|
||||
"""
|
||||
from django.conf.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
|
||||
template_name = {"template_name": "api/login.html"}
|
||||
|
||||
urlpatterns = patterns("django.contrib.auth.views",
|
||||
url(r"^login/$", "login", template_name, name="login"),
|
||||
url(r"^logout/$", "logout", template_name, name="logout"),
|
||||
)
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||
|
||||
|
||||
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
||||
"""
|
||||
Same as Django's standard shortcut, but make sure to raise 404
|
||||
if the filter_kwargs don't match the required types.
|
||||
"""
|
||||
try:
|
||||
return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
|
||||
except (TypeError, ValueError):
|
||||
raise Http404
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
|
||||
|
||||
def get_breadcrumbs(url):
|
||||
"""
|
||||
Given a url returns a list of breadcrumbs, which are each a
|
||||
tuple of (name, url).
|
||||
"""
|
||||
|
||||
from taiga.base.api.settings import api_settings
|
||||
from taiga.base.api.views import APIView
|
||||
|
||||
view_name_func = api_settings.VIEW_NAME_FUNCTION
|
||||
|
||||
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
|
||||
"""
|
||||
Add tuples of (name, url) to the breadcrumbs list,
|
||||
progressively chomping off parts of the url.
|
||||
"""
|
||||
|
||||
try:
|
||||
(view, unused_args, unused_kwargs) = resolve(url)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# Check if this is a REST framework view,
|
||||
# and if so add it to the breadcrumbs
|
||||
cls = getattr(view, "cls", None)
|
||||
if cls is not None and issubclass(cls, APIView):
|
||||
# Don't list the same view twice in a row.
|
||||
# Probably an optional trailing slash.
|
||||
if not seen or seen[-1] != view:
|
||||
suffix = getattr(view, "suffix", None)
|
||||
name = view_name_func(cls, suffix)
|
||||
breadcrumbs_list.insert(0, (name, prefix + url))
|
||||
seen.append(view)
|
||||
|
||||
if url == "":
|
||||
# All done
|
||||
return breadcrumbs_list
|
||||
|
||||
elif url.endswith("/"):
|
||||
# Drop trailing slash off the end and continue to try to
|
||||
# resolve more breadcrumbs
|
||||
url = url.rstrip("/")
|
||||
return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen)
|
||||
|
||||
# Drop trailing non-slash off the end and continue to try to
|
||||
# resolve more breadcrumbs
|
||||
url = url[:url.rfind("/") + 1]
|
||||
return breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen)
|
||||
|
||||
prefix = get_script_prefix().rstrip("/")
|
||||
url = url[len(prefix):]
|
||||
return breadcrumbs_recursive(url, [], prefix, [])
|
|
@ -0,0 +1,81 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Helper classes for parsers.
|
||||
"""
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.functional import Promise
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from taiga.base.api.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
import types
|
||||
import json
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
"""
|
||||
JSONEncoder subclass that knows how to encode date/time/timedelta,
|
||||
decimal types, and generators.
|
||||
"""
|
||||
def default(self, o):
|
||||
# For Date Time string spec, see ECMA 262
|
||||
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||
if isinstance(o, Promise):
|
||||
return force_text(o)
|
||||
elif isinstance(o, datetime.datetime):
|
||||
r = o.isoformat()
|
||||
if o.microsecond:
|
||||
r = r[:23] + r[26:]
|
||||
if r.endswith("+00:00"):
|
||||
r = r[:-6] + "Z"
|
||||
return r
|
||||
elif isinstance(o, datetime.date):
|
||||
return o.isoformat()
|
||||
elif isinstance(o, datetime.time):
|
||||
if timezone and timezone.is_aware(o):
|
||||
raise ValueError("JSON can't represent timezone-aware times.")
|
||||
r = o.isoformat()
|
||||
if o.microsecond:
|
||||
r = r[:12]
|
||||
return r
|
||||
elif isinstance(o, datetime.timedelta):
|
||||
return str(o.total_seconds())
|
||||
elif isinstance(o, decimal.Decimal):
|
||||
return str(o)
|
||||
elif isinstance(o, QuerySet):
|
||||
return list(o)
|
||||
elif hasattr(o, "tolist"):
|
||||
return o.tolist()
|
||||
elif hasattr(o, "__getitem__"):
|
||||
try:
|
||||
return dict(o)
|
||||
except:
|
||||
pass
|
||||
elif hasattr(o, "__iter__"):
|
||||
return [i for i in o]
|
||||
return super(JSONEncoder, self).default(o)
|
||||
|
||||
|
||||
SafeDumper = None
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Utility functions to return a formatted name and description for a given view.
|
||||
"""
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from taiga.base.api.settings import api_settings
|
||||
|
||||
from textwrap import dedent
|
||||
import re
|
||||
|
||||
# Markdown is optional
|
||||
try:
|
||||
import markdown
|
||||
|
||||
def apply_markdown(text):
|
||||
"""
|
||||
Simple wrapper around :func:`markdown.markdown` to set the base level
|
||||
of '#' style headers to <h2>.
|
||||
"""
|
||||
extensions = ["headerid(level=2)"]
|
||||
safe_mode = False
|
||||
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
|
||||
return md.convert(text)
|
||||
|
||||
except ImportError:
|
||||
apply_markdown = None
|
||||
|
||||
|
||||
def remove_trailing_string(content, trailing):
|
||||
"""
|
||||
Strip trailing component `trailing` from `content` if it exists.
|
||||
Used when generating names from view classes.
|
||||
"""
|
||||
if content.endswith(trailing) and content != trailing:
|
||||
return content[:-len(trailing)]
|
||||
return content
|
||||
|
||||
|
||||
def dedent(content):
|
||||
"""
|
||||
Remove leading indent from a block of text.
|
||||
Used when generating descriptions from docstrings.
|
||||
|
||||
Note that python's `textwrap.dedent` doesn't quite cut it,
|
||||
as it fails to dedent multiline docstrings that include
|
||||
unindented text on the initial line.
|
||||
"""
|
||||
whitespace_counts = [len(line) - len(line.lstrip(" "))
|
||||
for line in content.splitlines()[1:] if line.lstrip()]
|
||||
|
||||
# unindent the content if needed
|
||||
if whitespace_counts:
|
||||
whitespace_pattern = "^" + (" " * min(whitespace_counts))
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), "", content)
|
||||
|
||||
return content.strip()
|
||||
|
||||
def camelcase_to_spaces(content):
|
||||
"""
|
||||
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
||||
Used when generating names from view classes.
|
||||
"""
|
||||
camelcase_boundry = "(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))"
|
||||
content = re.sub(camelcase_boundry, " \\1", content).strip()
|
||||
return " ".join(content.split("_")).title()
|
||||
|
||||
def markup_description(description):
|
||||
"""
|
||||
Apply HTML markup to the given description.
|
||||
"""
|
||||
if apply_markdown:
|
||||
description = apply_markdown(description)
|
||||
else:
|
||||
description = escape(description).replace("\n", "<br />")
|
||||
return mark_safe(description)
|
|
@ -0,0 +1,107 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2014, Tom Christie
|
||||
|
||||
"""
|
||||
Handling of media types, as found in HTTP Content-Type and Accept headers.
|
||||
|
||||
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
||||
"""
|
||||
from django.http.multipartparser import parse_header
|
||||
|
||||
from taiga.base.api import HTTP_HEADER_ENCODING
|
||||
|
||||
|
||||
def media_type_matches(lhs, rhs):
|
||||
"""
|
||||
Returns ``True`` if the media type in the first argument <= the
|
||||
media type in the second argument. The media types are strings
|
||||
as described by the HTTP spec.
|
||||
|
||||
Valid media type strings include:
|
||||
|
||||
'application/json; indent=4'
|
||||
'application/json'
|
||||
'text/*'
|
||||
'*/*'
|
||||
"""
|
||||
lhs = _MediaType(lhs)
|
||||
rhs = _MediaType(rhs)
|
||||
return lhs.match(rhs)
|
||||
|
||||
|
||||
def order_by_precedence(media_type_lst):
|
||||
"""
|
||||
Returns a list of sets of media type strings, ordered by precedence.
|
||||
Precedence is determined by how specific a media type is:
|
||||
|
||||
3. 'type/subtype; param=val'
|
||||
2. 'type/subtype'
|
||||
1. 'type/*'
|
||||
0. '*/*'
|
||||
"""
|
||||
ret = [set(), set(), set(), set()]
|
||||
for media_type in media_type_lst:
|
||||
precedence = _MediaType(media_type).precedence
|
||||
ret[3 - precedence].add(media_type)
|
||||
return [media_types for media_types in ret if media_types]
|
||||
|
||||
|
||||
class _MediaType(object):
|
||||
def __init__(self, media_type_str):
|
||||
if media_type_str is None:
|
||||
media_type_str = ''
|
||||
self.orig = media_type_str
|
||||
self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING))
|
||||
self.main_type, sep, self.sub_type = self.full_type.partition("/")
|
||||
|
||||
def match(self, other):
|
||||
"""Return true if this MediaType satisfies the given MediaType."""
|
||||
for key in self.params.keys():
|
||||
if key != "q" and other.params.get(key, None) != self.params.get(key, None):
|
||||
return False
|
||||
|
||||
if self.sub_type != "*" and other.sub_type != "*" and other.sub_type != self.sub_type:
|
||||
return False
|
||||
|
||||
if self.main_type != "*" and other.main_type != "*" and other.main_type != self.main_type:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def precedence(self):
|
||||
"""
|
||||
Return a precedence level from 0-3 for the media type given how specific it is.
|
||||
"""
|
||||
if self.main_type == "*":
|
||||
return 0
|
||||
elif self.sub_type == "*":
|
||||
return 1
|
||||
elif not self.params or self.params.keys() == ["q"]:
|
||||
return 2
|
||||
return 3
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode("utf-8")
|
||||
|
||||
def __unicode__(self):
|
||||
ret = "%s/%s" % (self.main_type, self.sub_type)
|
||||
for key, val in self.params.items():
|
||||
ret += "; %s=%s" % (key, val)
|
||||
return ret
|
|
@ -19,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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
|
@ -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')
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
def patch_serializer():
|
||||
from rest_framework import serializers
|
||||
if hasattr(serializers.BaseSerializer, "_patched"):
|
||||
return
|
||||
|
||||
def to_native(self, obj):
|
||||
"""
|
||||
Serialize objects -> primitives.
|
||||
"""
|
||||
ret = self._dict_class()
|
||||
ret.fields = self._dict_class()
|
||||
ret.empty = obj is None
|
||||
|
||||
for field_name, field in self.fields.items():
|
||||
field.initialize(parent=self, field_name=field_name)
|
||||
key = self.get_field_key(field_name)
|
||||
ret.fields[key] = field
|
||||
|
||||
if obj is not None:
|
||||
value = field.field_to_native(obj, field_name)
|
||||
ret[key] = value
|
||||
|
||||
return ret
|
||||
|
||||
serializers.BaseSerializer._patched = True
|
||||
serializers.BaseSerializer.to_native = to_native
|
||||
|
||||
|
||||
def patch_restframework():
|
||||
from rest_framework import fields
|
||||
fields.strip_multiple_choice_msg = lambda x: x
|
|
@ -19,6 +19,8 @@ from collections import namedtuple
|
|||
|
||||
from django.db import connection
|
||||
|
||||
from 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)
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.forms import widgets
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from .neighbors import get_neighbors
|
||||
|
||||
|
||||
class TagsField(serializers.WritableField):
|
||||
"""
|
||||
Pickle objects serializer.
|
||||
"""
|
||||
def to_native(self, obj):
|
||||
return obj
|
||||
|
||||
def from_native(self, data):
|
||||
if not data:
|
||||
return data
|
||||
|
||||
ret = sum([tag.split(",") for tag in data], [])
|
||||
return ret
|
||||
|
||||
|
||||
class JsonField(serializers.WritableField):
|
||||
"""
|
||||
Json objects serializer.
|
||||
"""
|
||||
widget = widgets.Textarea
|
||||
|
||||
def to_native(self, obj):
|
||||
return obj
|
||||
|
||||
def from_native(self, data):
|
||||
return data
|
||||
|
||||
|
||||
class PgArrayField(serializers.WritableField):
|
||||
"""
|
||||
PgArray objects serializer.
|
||||
"""
|
||||
widget = widgets.Textarea
|
||||
|
||||
def to_native(self, obj):
|
||||
return obj
|
||||
|
||||
def from_native(self, data):
|
||||
return data
|
||||
|
||||
|
||||
class TagsColorsField(serializers.WritableField):
|
||||
"""
|
||||
PgArray objects serializer.
|
||||
"""
|
||||
widget = widgets.Textarea
|
||||
|
||||
def to_native(self, obj):
|
||||
return dict(obj)
|
||||
|
||||
def from_native(self, data):
|
||||
return list(data.items())
|
||||
|
||||
|
||||
class NeighborsSerializerMixin:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors")
|
||||
|
||||
def serialize_neighbor(self, neighbor):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_neighbors(self, obj):
|
||||
view, request = self.context.get("view", None), self.context.get("request", None)
|
||||
if view and request:
|
||||
queryset = view.filter_queryset(view.get_queryset())
|
||||
left, right = get_neighbors(obj, results_set=queryset)
|
||||
else:
|
||||
left = right = None
|
||||
|
||||
return {
|
||||
"previous": self.serialize_neighbor(left),
|
||||
"next": self.serialize_neighbor(right)
|
||||
}
|
||||
|
||||
|
||||
class Serializer(serializers.Serializer):
|
||||
def skip_field_validation(self, field, attrs, source):
|
||||
return source not in attrs and (field.partial or not field.required)
|
||||
|
||||
def perform_validation(self, attrs):
|
||||
"""
|
||||
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
||||
"""
|
||||
for field_name, field in self.fields.items():
|
||||
if field_name in self._errors:
|
||||
continue
|
||||
|
||||
source = field.source or field_name
|
||||
if self.skip_field_validation(field, attrs, source):
|
||||
continue
|
||||
|
||||
try:
|
||||
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
||||
if validate_method:
|
||||
attrs = validate_method(attrs, source)
|
||||
except serializers.ValidationError as err:
|
||||
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
|
||||
|
||||
# If there are already errors, we don't run .validate() because
|
||||
# field-validation failed and thus `attrs` may not be complete.
|
||||
# which in turn can cause inconsistent validation errors.
|
||||
if not self._errors:
|
||||
try:
|
||||
attrs = self.validate(attrs)
|
||||
except serializers.ValidationError as err:
|
||||
if hasattr(err, 'message_dict'):
|
||||
for field_name, error_messages in err.message_dict.items():
|
||||
self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
|
||||
elif hasattr(err, 'messages'):
|
||||
self._errors['non_field_errors'] = err.messages
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class ModelSerializer(serializers.ModelSerializer):
|
||||
def perform_validation(self, attrs):
|
||||
for attr in attrs:
|
||||
field = self.fields.get(attr, None)
|
||||
if field:
|
||||
field.required = True
|
||||
return super().perform_validation(attrs)
|
||||
|
||||
def save(self, **kwargs):
|
||||
"""
|
||||
Due to DRF bug with M2M fields we refresh object state from database
|
||||
directly if object is models.Model type and it contains m2m fields
|
||||
|
||||
See: https://github.com/tomchristie/django-rest-framework/issues/1556
|
||||
"""
|
||||
self.object = super(serializers.ModelSerializer, self).save(**kwargs)
|
||||
model = self.Meta.model
|
||||
if model._meta.model._meta.local_many_to_many and self.object.pk:
|
||||
self.object = model.objects.get(pk=self.object.pk)
|
||||
return self.object
|
|
@ -0,0 +1,89 @@
|
|||
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
|
||||
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This code is partially taken from django-rest-framework:
|
||||
# Copyright (c) 2011-2015, Tom Christie
|
||||
|
||||
|
||||
"""
|
||||
Descriptive HTTP status codes, for code readability.
|
||||
|
||||
See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||
And RFC 6585 - http://tools.ietf.org/html/rfc6585
|
||||
"""
|
||||
|
||||
|
||||
def is_informational(code):
|
||||
return code >= 100 and code <= 199
|
||||
|
||||
def is_success(code):
|
||||
return code >= 200 and code <= 299
|
||||
|
||||
def is_redirect(code):
|
||||
return code >= 300 and code <= 399
|
||||
|
||||
def is_client_error(code):
|
||||
return code >= 400 and code <= 499
|
||||
|
||||
def is_server_error(code):
|
||||
return code >= 500 and code <= 599
|
||||
|
||||
|
||||
HTTP_100_CONTINUE = 100
|
||||
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||
HTTP_200_OK = 200
|
||||
HTTP_201_CREATED = 201
|
||||
HTTP_202_ACCEPTED = 202
|
||||
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
|
||||
HTTP_204_NO_CONTENT = 204
|
||||
HTTP_205_RESET_CONTENT = 205
|
||||
HTTP_206_PARTIAL_CONTENT = 206
|
||||
HTTP_300_MULTIPLE_CHOICES = 300
|
||||
HTTP_301_MOVED_PERMANENTLY = 301
|
||||
HTTP_302_FOUND = 302
|
||||
HTTP_303_SEE_OTHER = 303
|
||||
HTTP_304_NOT_MODIFIED = 304
|
||||
HTTP_305_USE_PROXY = 305
|
||||
HTTP_306_RESERVED = 306
|
||||
HTTP_307_TEMPORARY_REDIRECT = 307
|
||||
HTTP_400_BAD_REQUEST = 400
|
||||
HTTP_401_UNAUTHORIZED = 401
|
||||
HTTP_402_PAYMENT_REQUIRED = 402
|
||||
HTTP_403_FORBIDDEN = 403
|
||||
HTTP_404_NOT_FOUND = 404
|
||||
HTTP_405_METHOD_NOT_ALLOWED = 405
|
||||
HTTP_406_NOT_ACCEPTABLE = 406
|
||||
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
|
||||
HTTP_408_REQUEST_TIMEOUT = 408
|
||||
HTTP_409_CONFLICT = 409
|
||||
HTTP_410_GONE = 410
|
||||
HTTP_411_LENGTH_REQUIRED = 411
|
||||
HTTP_412_PRECONDITION_FAILED = 412
|
||||
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
|
||||
HTTP_414_REQUEST_URI_TOO_LONG = 414
|
||||
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
|
||||
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
|
||||
HTTP_417_EXPECTATION_FAILED = 417
|
||||
HTTP_428_PRECONDITION_REQUIRED = 428
|
||||
HTTP_429_TOO_MANY_REQUESTS = 429
|
||||
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
||||
HTTP_501_NOT_IMPLEMENTED = 501
|
||||
HTTP_502_BAD_GATEWAY = 502
|
||||
HTTP_503_SERVICE_UNAVAILABLE = 503
|
||||
HTTP_504_GATEWAY_TIMEOUT = 504
|
||||
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
|
||||
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
|
|
@ -14,7 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# 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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -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,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
|
@ -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()
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue