From 5a45db23fcc6ad51476713449f302cdb0a5de6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 18 Mar 2015 11:11:46 +0100 Subject: [PATCH 01/96] Temporary disable public projects on listings --- taiga/base/filters.py | 13 +++-- .../test_projects_resource.py | 50 +++++++++---------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 43c0219b..5f7c50e8 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -212,10 +212,17 @@ class CanViewProjectObjFilterBackend(FilterBackend): projects_list = [membership.project_id for membership in memberships_qs] - qs = qs.filter((Q(id__in=projects_list) | - Q(public_permissions__contains=["view_project"]))) + #### + # TODO: Temporary fix for visualization of public projects in the interface + qs = qs.filter(id__in=projects_list) else: - qs = qs.filter(anon_permissions__contains=["view_project"]) + qs = qs.none() + + # qs = qs.filter((Q(id__in=projects_list) | + # Q(public_permissions__contains=["view_project"]))) + # else: + # qs = qs.filter(anon_permissions__contains=["view_project"]) + #### return super().filter_queryset(request, qs.distinct(), view) diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 0c97c952..0c057f87 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -97,9 +97,9 @@ def test_project_retrieve(client, data): ] results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [401, 403, 200, 200] @@ -140,21 +140,21 @@ def test_project_list(client, data): response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 2 + assert len(projects_data) == 0 assert response.status_code == 200 client.login(data.registered_user) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 2 + assert len(projects_data) == 0 assert response.status_code == 200 client.login(data.project_member_with_perms) response = client.get(url) projects_data = json.loads(response.content.decode('utf-8')) - assert len(projects_data) == 3 + assert len(projects_data) == 2 assert response.status_code == 200 client.login(data.project_owner) @@ -191,9 +191,9 @@ def test_project_action_stats(client, data): data.project_owner ] results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [404, 404, 200, 200] @@ -210,9 +210,9 @@ def test_project_action_star(client, data): data.project_owner ] results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 200, 200, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 200, 200, 200] + assert results == [404, 404, 200, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] @@ -229,9 +229,9 @@ def test_project_action_unstar(client, data): data.project_owner ] results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 200, 200, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 200, 200, 200] + assert results == [404, 404, 200, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 200, 200] @@ -248,9 +248,9 @@ def test_project_action_issues_stats(client, data): data.project_owner ] results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [404, 404, 200, 200] @@ -267,9 +267,9 @@ def test_project_action_issues_filters_data(client, data): data.project_owner ] results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200] + assert results == [404, 404, 200, 200] results = helper_test_http_method(client, 'get', private2_url, None, users) assert results == [404, 404, 200, 200] @@ -288,9 +288,9 @@ def test_project_action_fans(client, data): ] results = helper_test_http_method_and_count(client, 'get', public_url, None, users) - assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)] + assert results == [(404, 1), (404, 1), (404, 1), (404, 1), (200, 2)] results = helper_test_http_method_and_count(client, 'get', private1_url, None, users) - assert results == [(200, 2), (200, 2), (200, 2), (200, 2), (200, 2)] + assert results == [(404, 1), (404, 1), (404, 1), (200, 2), (200, 2)] results = helper_test_http_method_and_count(client, 'get', private2_url, None, users) assert results == [(404, 1), (404, 1), (404, 1), (200, 2), (200, 2)] @@ -336,9 +336,9 @@ def test_project_action_create_template(client, data): }) results = helper_test_http_method(client, 'post', public_url, template_data, users) - assert results == [401, 403, 403, 403, 403, 201] + assert results == [404, 404, 404, 404, 403, 201] results = helper_test_http_method(client, 'post', private1_url, template_data, users) - assert results == [401, 403, 403, 403, 403, 201] + assert results == [404, 404, 404, 403, 403, 201] results = helper_test_http_method(client, 'post', private2_url, template_data, users) assert results == [404, 404, 404, 403, 403, 201] @@ -383,10 +383,10 @@ def test_regenerate_userstories_csv_uuid(client, data): data.project_owner ] results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 403, 403, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 403, 403, 200] + assert results == [404, 404, 403, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 403, 200] @@ -404,10 +404,10 @@ def test_regenerate_tasks_csv_uuid(client, data): data.project_owner ] results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 403, 403, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 403, 403, 200] + assert results == [404, 404, 403, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 403, 200] @@ -425,10 +425,10 @@ def test_regenerate_issues_csv_uuid(client, data): data.project_owner ] results = helper_test_http_method(client, 'post', public_url, None, users) - assert results == [401, 403, 403, 200] + assert results == [404, 404, 404, 200] results = helper_test_http_method(client, 'post', private1_url, None, users) - assert results == [401, 403, 403, 200] + assert results == [404, 404, 403, 200] results = helper_test_http_method(client, 'post', private2_url, None, users) assert results == [404, 404, 403, 200] From 3877411c412435df5460db01b95dfe32489e9671 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 18 Mar 2015 13:52:56 +0100 Subject: [PATCH 02/96] Fixing empty body bug in github webhooks --- taiga/hooks/github/event_hooks.py | 3 +++ tests/integration/test_hooks_github.py | 1 + 2 files changed, 4 insertions(+) diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py index 8123bf00..50c465c5 100644 --- a/taiga/hooks/github/event_hooks.py +++ b/taiga/hooks/github/event_hooks.py @@ -91,6 +91,9 @@ class PushEventHook(BaseEventHook): def replace_github_references(project_url, wiki_text): + if wiki_text == None: + wiki_text = "" + template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 6d85cca4..32412d0b 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -455,3 +455,4 @@ def test_replace_github_references(): assert event_hooks.replace_github_references("project-url", " #2 ") == " [GitHub#2](project-url/issues/2) " assert event_hooks.replace_github_references("project-url", " #2") == " [GitHub#2](project-url/issues/2)" assert event_hooks.replace_github_references("project-url", "#test") == "#test" + assert event_hooks.replace_github_references("project-url", None) == "" From 74aa4e58919797ee8021807c26ca7876b8c06dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 17 Mar 2015 19:05:23 +0100 Subject: [PATCH 03/96] Fix visualization of blocked_note and is_blokked fields --- ...bloked_note_and_is_blocked_in_snapshots.py | 80 +++++++++++++++++++ .../emails/includes/fields_diff-html.jinja | 16 ++-- 2 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py diff --git a/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py b/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py new file mode 100644 index 00000000..8f8f3ed6 --- /dev/null +++ b/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.core.exceptions import ObjectDoesNotExist +from taiga.projects.history.services import (make_key_from_model_object, + get_model_from_key, + get_pk_from_key) + + +def update_many(objects, fields=[], using="default"): + """Update list of Django objects in one SQL query, optionally only + overwrite the given fields (as names, e.g. fields=["foo"]). + Objects must be of the same Django model. Note that save is not + called and signals on the model are not raised.""" + if not objects: + return + + import django.db.models + from django.db import connections + con = connections[using] + + names = fields + meta = objects[0]._meta + fields = [f for f in meta.fields if not isinstance(f, django.db.models.AutoField) and (not names or f.name in names)] + + if not fields: + raise ValueError("No fields to update, field names are %s." % names) + + fields_with_pk = fields + [meta.pk] + parameters = [] + for o in objects: + parameters.append(tuple(f.get_db_prep_save(f.pre_save(o, True), connection=con) for f in fields_with_pk)) + + table = meta.db_table + assignments = ",".join(("%s=%%s"% con.ops.quote_name(f.column)) for f in fields) + con.cursor().executemany( + "update %s set %s where %s=%%s" % (table, assignments, con.ops.quote_name(meta.pk.column)), + parameters) + + +def set_current_values_of_blocked_note_and_is_blocked_to_the_last_snapshot(apps, schema_editor): + HistoryEntry = apps.get_model("history", "HistoryEntry") + updatingEntries = [] + + for history_entry in HistoryEntry.objects.filter(is_snapshot=True).order_by("created_at"): + model = get_model_from_key(history_entry.key) + pk = get_pk_from_key(history_entry.key) + try: + print("Fixing history_entry: ", history_entry.created_at) + obj = model.objects.get(pk=pk) + save = False + if hasattr(obj, "is_blocked") and "is_blocked" not in history_entry.snapshot: + history_entry.snapshot["is_blocked"] = obj.is_blocked + save = True + + if hasattr(obj, "blocked_note") and "blocked_note" not in history_entry.snapshot: + history_entry.snapshot["blocked_note"] = obj.blocked_note + save = True + + if save: + updatingEntries.append(history_entry) + + except ObjectDoesNotExist as e: + print("Ignoring {}".format(history_entry.pk)) + + update_many(updatingEntries) + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0006_fix_json_field_not_null'), + ('userstories', '0009_remove_userstory_is_archived'), + ('tasks', '0005_auto_20150114_0954'), + ('issues', '0004_auto_20150114_0954'), + ] + + operations = [ + migrations.RunPython(set_current_values_of_blocked_note_and_is_blocked_to_the_last_snapshot), + ] diff --git a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja index 84a2d462..667b66e7 100644 --- a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja +++ b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja @@ -1,6 +1,8 @@ {% set excluded_fields = [ "description", "description_html", + "blocked_note", + "blocked_note_html", "content", "content_html", "backlog_order", @@ -109,19 +111,11 @@ {% endif %} - {# DESCRIPTIONS #} - {% elif field_name in ["description_diff"] %} + {# DESCRIPTIONS, CONTENT, BLOCKED_NOTE #} + {% elif field_name in ["description_diff", "content_diff", "blocked_note_diff"] %} -

{{ _("Description diff") }}

-

{{ mdrender(project, values.1) }}

- - - {# CONTENT #} - {% elif field_name in ["content_diff"] %} - - -

{{ _("Content diff") }}

+

{{ verbose_name(obj_class, field_name) }}

{{ mdrender(project, values.1) }}

From b15601be7540808bb435feddd2917d48c3899852 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 18 Mar 2015 16:00:13 +0100 Subject: [PATCH 04/96] Fixing migration --- ...bloked_note_and_is_blocked_in_snapshots.py | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py b/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py index 8f8f3ed6..dafe32ed 100644 --- a/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py +++ b/taiga/projects/history/migrations/0007_set_bloked_note_and_is_blocked_in_snapshots.py @@ -8,40 +8,8 @@ from taiga.projects.history.services import (make_key_from_model_object, get_pk_from_key) -def update_many(objects, fields=[], using="default"): - """Update list of Django objects in one SQL query, optionally only - overwrite the given fields (as names, e.g. fields=["foo"]). - Objects must be of the same Django model. Note that save is not - called and signals on the model are not raised.""" - if not objects: - return - - import django.db.models - from django.db import connections - con = connections[using] - - names = fields - meta = objects[0]._meta - fields = [f for f in meta.fields if not isinstance(f, django.db.models.AutoField) and (not names or f.name in names)] - - if not fields: - raise ValueError("No fields to update, field names are %s." % names) - - fields_with_pk = fields + [meta.pk] - parameters = [] - for o in objects: - parameters.append(tuple(f.get_db_prep_save(f.pre_save(o, True), connection=con) for f in fields_with_pk)) - - table = meta.db_table - assignments = ",".join(("%s=%%s"% con.ops.quote_name(f.column)) for f in fields) - con.cursor().executemany( - "update %s set %s where %s=%%s" % (table, assignments, con.ops.quote_name(meta.pk.column)), - parameters) - - def set_current_values_of_blocked_note_and_is_blocked_to_the_last_snapshot(apps, schema_editor): HistoryEntry = apps.get_model("history", "HistoryEntry") - updatingEntries = [] for history_entry in HistoryEntry.objects.filter(is_snapshot=True).order_by("created_at"): model = get_model_from_key(history_entry.key) @@ -59,12 +27,11 @@ def set_current_values_of_blocked_note_and_is_blocked_to_the_last_snapshot(apps, save = True if save: - updatingEntries.append(history_entry) + history_entry.save() except ObjectDoesNotExist as e: print("Ignoring {}".format(history_entry.pk)) - update_many(updatingEntries) class Migration(migrations.Migration): From fcd5ccbd74e50da07910ebea95b4ea0f9cc34abd Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 23 Mar 2015 08:31:48 +0100 Subject: [PATCH 05/96] Fixing empty body bug in gitlab webhooks --- taiga/hooks/gitlab/event_hooks.py | 3 +++ tests/integration/test_hooks_gitlab.py | 1 + 2 files changed, 4 insertions(+) diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py index 426ab259..faa81df1 100644 --- a/taiga/hooks/gitlab/event_hooks.py +++ b/taiga/hooks/gitlab/event_hooks.py @@ -90,6 +90,9 @@ class PushEventHook(BaseEventHook): def replace_gitlab_references(project_url, wiki_text): + if wiki_text == None: + wiki_text = "" + template = "\g<1>[GitLab#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index 54a44d6d..10935c46 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -383,3 +383,4 @@ def test_replace_gitlab_references(): assert event_hooks.replace_gitlab_references("project-url", " #2 ") == " [GitLab#2](project-url/issues/2) " assert event_hooks.replace_gitlab_references("project-url", " #2") == " [GitLab#2](project-url/issues/2)" assert event_hooks.replace_gitlab_references("project-url", "#test") == "#test" + assert event_hooks.replace_gitlab_references("project-url", None) == "" From d2ba8b4327a0ca8046ecddb5b335881662eceb22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 23 Mar 2015 13:06:36 +0100 Subject: [PATCH 06/96] Fixed vhost name on Events rabbitmq backend --- taiga/events/backends/rabbitmq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/events/backends/rabbitmq.py b/taiga/events/backends/rabbitmq.py index a745a196..182d2548 100644 --- a/taiga/events/backends/rabbitmq.py +++ b/taiga/events/backends/rabbitmq.py @@ -40,7 +40,7 @@ def _make_rabbitmq_connection(url): vhost = parse_result.path return AmqpConnection(host=host, userid=user, - password=password, virtual_host=vhost) + password=password, virtual_host=vhost[1:]) class EventsPushBackend(base.BaseEventsPushBackend): From 466a17c1c0c2acc39e18a512bd9baa0fdc1197ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 26 Mar 2015 12:05:55 +0100 Subject: [PATCH 07/96] Update requirements-devel.txt --- requirements-devel.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index da456ec3..45c11b38 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -9,5 +9,3 @@ pytest-pythonpath==0.6 coverage==3.7.1 coveralls==0.4.2 django-slowdown==0.0.1 - -taiga-contrib-github-auth==0.0.3 From c178aacdb2a61ca0af4fde95a174b935df68df59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 26 Mar 2015 12:42:32 +0100 Subject: [PATCH 08/96] Update djmail to 0.10 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4afc973..ac564e0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ pillow==2.5.3 pytz==2014.4 six==1.8.0 amqp==1.4.6 -djmail==0.9 +djmail==0.10 django-pgjson==0.2.2 djorm-pgarray==1.0.4 django-jinja==1.0.4 From 7302af8eb70d14360805910377241b974311d215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 27 Mar 2015 12:45:02 +0100 Subject: [PATCH 09/96] Add copyright and license terms --- taiga/projects/validators.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index 8ad7b3ef..700fa3ab 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -1,3 +1,19 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers From 7beff9cab70d20c64834ee5e1112d66b9fcb75b7 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 7 Apr 2015 14:18:05 +0200 Subject: [PATCH 10/96] Fixing custom attributes notifications on name change --- .../emails/includes/fields_diff-html.jinja | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja index 667b66e7..88b56f41 100644 --- a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja +++ b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja @@ -184,21 +184,23 @@ {% endif %} {% if values.changed %} {% for attr in values['changed'] %} - - -

{{ attr.name }}

- - - {{ _("from") }}
- {{ attr.changes.value.0|linebreaksbr }} - - - - - {{ _("to") }}
- {{ attr.changes.value.1|linebreaksbr }} - - + {% if attr.changes.value%} + + +

{{ attr.name }}

+ + + {{ _("from") }}
+ {{ attr.changes.value.0|linebreaksbr }} + + + + + {{ _("to") }}
+ {{ attr.changes.value.1|linebreaksbr }} + + + {% endif %} {% endfor %} {% endif %} {% if values.deleted %} From 8868e89d6f0da1eb44ec08a41dcd21d58c33cd54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 30 Mar 2015 12:33:28 +0200 Subject: [PATCH 11/96] Force regenerate invitations uuids on dump load --- taiga/export_import/serializers.py | 2 +- taiga/export_import/service.py | 3 +-- tests/integration/test_importer_api.py | 24 +++++++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 7b27c427..6287e78d 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -425,7 +425,7 @@ class MembershipExportSerializer(serializers.ModelSerializer): class Meta: model = projects_models.Membership - exclude = ('id', 'project') + exclude = ('id', 'project', 'token') def full_clean(self, instance): return instance diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 8eb8cd42..79eeee8b 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -182,8 +182,7 @@ def store_membership(project, membership): if serialized.is_valid(): serialized.object.project = project serialized.object._importing = True - if not serialized.object.token: - serialized.object.token = str(uuid.uuid1()) + serialized.object.token = str(uuid.uuid1()) serialized.object.user = find_invited_user(serialized.object.email, default=serialized.object.user) serialized.save() diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 9cbb64c0..67f4407b 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -25,7 +25,7 @@ from .. import factories as f from django.apps import apps from taiga.base.utils import json -from taiga.projects.models import Project +from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task @@ -90,6 +90,28 @@ def test_valid_project_import_with_not_existing_memberships(client): assert len(response_data["memberships"]) == 2 +def test_valid_project_import_with_membership_uuid_rewrite(client): + user = f.UserFactory.create() + client.login(user) + + url = reverse("importer-list") + data = { + "name": "Imported project", + "description": "Imported project", + "memberships": [{ + "email": "with-uuid@email.com", + "role": "Role", + "token": "123", + }], + "roles": [{"name": "Role"}] + } + + response = client.post(url, json.dumps(data), content_type="application/json") + assert response.status_code == 201 + response_data = json.loads(response.content.decode("utf-8")) + assert Membership.objects.filter(email="with-uuid@email.com", token="123").count() == 0 + + def test_valid_project_import_with_extra_data(client): user = f.UserFactory.create() client.login(user) From 6b294c4ca9c63d9c7f3f11a2b9ebc79ee1bc3f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 7 Apr 2015 23:36:17 +0200 Subject: [PATCH 12/96] Remove unnecessary command --- .../attachments/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/migrate_attachments.py | 130 ------------------ 3 files changed, 130 deletions(-) delete mode 100644 taiga/projects/attachments/management/__init__.py delete mode 100644 taiga/projects/attachments/management/commands/__init__.py delete mode 100644 taiga/projects/attachments/management/commands/migrate_attachments.py diff --git a/taiga/projects/attachments/management/__init__.py b/taiga/projects/attachments/management/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/taiga/projects/attachments/management/commands/__init__.py b/taiga/projects/attachments/management/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/taiga/projects/attachments/management/commands/migrate_attachments.py b/taiga/projects/attachments/management/commands/migrate_attachments.py deleted file mode 100644 index 7362122b..00000000 --- a/taiga/projects/attachments/management/commands/migrate_attachments.py +++ /dev/null @@ -1,130 +0,0 @@ -import re - -from django.apps import apps -from django.core.management.base import BaseCommand, CommandError -from django.core.files import File -from django.conf import settings -from django.db import transaction - -from taiga.base.utils.iterators import iter_queryset - -url = """ -https://api-taiga.kaleidos.net/attachments/446/?user=8&token=9ac0fc593e9c07740975c6282e1e501189578faa -""" - -class Command(BaseCommand): - help = "Parses all objects and try replace old attachments url with one new" - - - trx = r"((?:https?)://api-taiga\.kaleidos\.net/attachments/(\d+)/[^\s\"]+)" - - @transaction.atomic - def handle(self, *args, **options): - settings.MEDIA_URL="https://media.taiga.io/" - - self.move_user_photo() - self.move_attachments() - self.process_userstories() - self.process_issues() - self.process_wiki() - self.process_tasks() - self.process_history() - - def move_attachments(self): - print("Moving all attachments to new location") - - Attachment = apps.get_model("attachments", "Attachment") - qs = Attachment.objects.all() - - for item in iter_queryset(qs): - try: - with transaction.atomic(): - old_file = item.attached_file - item.attached_file = File(old_file) - item.save() - except FileNotFoundError: - item.delete() - - def move_user_photo(self): - print("Moving all user photos to new location") - - User = apps.get_model("users", "User") - qs = User.objects.all() - - for item in iter_queryset(qs): - try: - with transaction.atomic(): - old_file = item.photo - item.photo = File(old_file) - item.save() - except FileNotFoundError: - pass - - def get_attachment_real_url(self, pk): - if isinstance(pk, str): - pk = int(pk) - - Attachment = apps.get_model("attachments", "Attachment") - return Attachment.objects.get(pk=pk).attached_file.url - - def replace_matches(self, data): - matches = re.findall(self.trx, data) - - original_data = data - - if len(matches) == 0: - return data - - for url, attachment_id in matches: - new_url = self.get_attachment_real_url(attachment_id) - print("Match {} replaced by {}".format(url, new_url)) - - try: - data = data.replace(url, self.get_attachment_real_url(attachment_id)) - except Exception as e: - print("Exception found but ignoring:", e) - - assert data != original_data - - return data - - def process_userstories(self): - UserStory = apps.get_model("userstories", "UserStory") - qs = UserStory.objects.all() - - for item in iter_queryset(qs): - description = self.replace_matches(item.description) - UserStory.objects.filter(pk=item.pk).update(description=description) - - def process_tasks(self): - Task = apps.get_model("tasks", "Task") - qs = Task.objects.all() - - for item in iter_queryset(qs): - description = self.replace_matches(item.description) - Task.objects.filter(pk=item.pk).update(description=description) - - def process_issues(self): - Issue = apps.get_model("issues", "Issue") - qs = Issue.objects.all() - - for item in iter_queryset(qs): - description = self.replace_matches(item.description) - Issue.objects.filter(pk=item.pk).update(description=description) - - def process_wiki(self): - WikiPage = apps.get_model("wiki", "WikiPage") - qs = WikiPage.objects.all() - - for item in iter_queryset(qs): - content = self.replace_matches(item.content) - WikiPage.objects.filter(pk=item.pk).update(content=content) - - def process_history(self): - HistoryEntry = apps.get_model("history", "HistoryEntry") - qs = HistoryEntry.objects.all() - - for item in iter_queryset(qs): - comment = self.replace_matches(item.comment) - comment_html = self.replace_matches(item.comment_html) - HistoryEntry.objects.filter(pk=item.pk).update(comment=comment, comment_html=comment_html) From 2c16c858f9be1f00065146b09b6cb39f3851081e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 8 Apr 2015 19:20:32 +0200 Subject: [PATCH 13/96] Add license --- taiga/projects/userstories/validators.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index b3691877..7c31670b 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -1,3 +1,19 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers From 027e15475d0fed7aeae5b0493340f7a9afe515aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 24 Mar 2015 19:50:48 +0100 Subject: [PATCH 14/96] Add manage_translations script --- .tx/config | 16 + locale/ca/LC_MESSAGES/django.po | 355 +++++ locale/en/LC_MESSAGES/django.po | 353 +++++ locale/es/LC_MESSAGES/django.po | 355 +++++ requirements-devel.txt | 2 + scripts/manage_translations.py | 246 +++ settings/common.py | 113 +- taiga/locale/ca/LC_MESSAGES/django.po | 1589 +++++++++++++++++++ taiga/locale/en/LC_MESSAGES/django.po | 1531 +++++++++++++++++++ taiga/locale/es/LC_MESSAGES/django.po | 2033 ++++++++++++++++++------- 10 files changed, 6038 insertions(+), 555 deletions(-) create mode 100644 .tx/config create mode 100644 locale/ca/LC_MESSAGES/django.po create mode 100644 locale/en/LC_MESSAGES/django.po create mode 100644 locale/es/LC_MESSAGES/django.po create mode 100644 scripts/manage_translations.py create mode 100644 taiga/locale/ca/LC_MESSAGES/django.po create mode 100644 taiga/locale/en/LC_MESSAGES/django.po diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..6f1602c1 --- /dev/null +++ b/.tx/config @@ -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//LC_MESSAGES/django.po +source_file = locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO + +[taiga-back.taiga] +file_filter = taiga/locale//LC_MESSAGES/django.po +source_file = taiga/locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO diff --git a/locale/ca/LC_MESSAGES/django.po b/locale/ca/LC_MESSAGES/django.po new file mode 100644 index 00000000..2df32355 --- /dev/null +++ b/locale/ca/LC_MESSAGES/django.po @@ -0,0 +1,355 @@ +# taiga-back.main +# Copyright (C) 2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"PO-Revision-Date: 2015-03-24 18:08+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Catalan (http://www.transifex.com/projects/p/taiga-back/" +"language/ca/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: settings/common.py:73 +msgid "Afrikaans" +msgstr "" + +#: settings/common.py:74 +msgid "Arabic" +msgstr "" + +#: settings/common.py:75 +msgid "Asturian" +msgstr "" + +#: settings/common.py:76 +msgid "Azerbaijani" +msgstr "" + +#: settings/common.py:77 +msgid "Bulgarian" +msgstr "" + +#: settings/common.py:78 +msgid "Belarusian" +msgstr "" + +#: settings/common.py:79 +msgid "Bengali" +msgstr "" + +#: settings/common.py:80 +msgid "Breton" +msgstr "" + +#: settings/common.py:81 +msgid "Bosnian" +msgstr "" + +#: settings/common.py:82 +msgid "Catalan" +msgstr "" + +#: settings/common.py:83 +msgid "Czech" +msgstr "" + +#: settings/common.py:84 +msgid "Welsh" +msgstr "" + +#: settings/common.py:85 +msgid "Danish" +msgstr "" + +#: settings/common.py:86 +msgid "German" +msgstr "" + +#: settings/common.py:87 +msgid "Greek" +msgstr "" + +#: settings/common.py:88 +msgid "English" +msgstr "" + +#: settings/common.py:89 +msgid "Australian English" +msgstr "" + +#: settings/common.py:90 +msgid "British English" +msgstr "" + +#: settings/common.py:91 +msgid "Esperanto" +msgstr "" + +#: settings/common.py:92 +msgid "Spanish" +msgstr "" + +#: settings/common.py:93 +msgid "Argentinian Spanish" +msgstr "" + +#: settings/common.py:94 +msgid "Mexican Spanish" +msgstr "" + +#: settings/common.py:95 +msgid "Nicaraguan Spanish" +msgstr "" + +#: settings/common.py:96 +msgid "Venezuelan Spanish" +msgstr "" + +#: settings/common.py:97 +msgid "Estonian" +msgstr "" + +#: settings/common.py:98 +msgid "Basque" +msgstr "" + +#: settings/common.py:99 +msgid "Persian" +msgstr "" + +#: settings/common.py:100 +msgid "Finnish" +msgstr "" + +#: settings/common.py:101 +msgid "French" +msgstr "" + +#: settings/common.py:102 +msgid "Frisian" +msgstr "" + +#: settings/common.py:103 +msgid "Irish" +msgstr "" + +#: settings/common.py:104 +msgid "Galician" +msgstr "" + +#: settings/common.py:105 +msgid "Hebrew" +msgstr "" + +#: settings/common.py:106 +msgid "Hindi" +msgstr "" + +#: settings/common.py:107 +msgid "Croatian" +msgstr "" + +#: settings/common.py:108 +msgid "Hungarian" +msgstr "" + +#: settings/common.py:109 +msgid "Interlingua" +msgstr "" + +#: settings/common.py:110 +msgid "Indonesian" +msgstr "" + +#: settings/common.py:111 +msgid "Ido" +msgstr "" + +#: settings/common.py:112 +msgid "Icelandic" +msgstr "" + +#: settings/common.py:113 +msgid "Italian" +msgstr "" + +#: settings/common.py:114 +msgid "Japanese" +msgstr "" + +#: settings/common.py:115 +msgid "Georgian" +msgstr "" + +#: settings/common.py:116 +msgid "Kazakh" +msgstr "" + +#: settings/common.py:117 +msgid "Khmer" +msgstr "" + +#: settings/common.py:118 +msgid "Kannada" +msgstr "" + +#: settings/common.py:119 +msgid "Korean" +msgstr "" + +#: settings/common.py:120 +msgid "Luxembourgish" +msgstr "" + +#: settings/common.py:121 +msgid "Lithuanian" +msgstr "" + +#: settings/common.py:122 +msgid "Latvian" +msgstr "" + +#: settings/common.py:123 +msgid "Macedonian" +msgstr "" + +#: settings/common.py:124 +msgid "Malayalam" +msgstr "" + +#: settings/common.py:125 +msgid "Mongolian" +msgstr "" + +#: settings/common.py:126 +msgid "Marathi" +msgstr "" + +#: settings/common.py:127 +msgid "Burmese" +msgstr "" + +#: settings/common.py:128 +msgid "Norwegian Bokmal" +msgstr "" + +#: settings/common.py:129 +msgid "Nepali" +msgstr "" + +#: settings/common.py:130 +msgid "Dutch" +msgstr "" + +#: settings/common.py:131 +msgid "Norwegian Nynorsk" +msgstr "" + +#: settings/common.py:132 +msgid "Ossetic" +msgstr "" + +#: settings/common.py:133 +msgid "Punjabi" +msgstr "" + +#: settings/common.py:134 +msgid "Polish" +msgstr "" + +#: settings/common.py:135 +msgid "Portuguese" +msgstr "" + +#: settings/common.py:136 +msgid "Brazilian Portuguese" +msgstr "" + +#: settings/common.py:137 +msgid "Romanian" +msgstr "" + +#: settings/common.py:138 +msgid "Russian" +msgstr "" + +#: settings/common.py:139 +msgid "Slovak" +msgstr "" + +#: settings/common.py:140 +msgid "Slovenian" +msgstr "" + +#: settings/common.py:141 +msgid "Albanian" +msgstr "" + +#: settings/common.py:142 +msgid "Serbian" +msgstr "" + +#: settings/common.py:143 +msgid "Serbian Latin" +msgstr "" + +#: settings/common.py:144 +msgid "Swedish" +msgstr "" + +#: settings/common.py:145 +msgid "Swahili" +msgstr "" + +#: settings/common.py:146 +msgid "Tamil" +msgstr "" + +#: settings/common.py:147 +msgid "Telugu" +msgstr "" + +#: settings/common.py:148 +msgid "Thai" +msgstr "" + +#: settings/common.py:149 +msgid "Turkish" +msgstr "" + +#: settings/common.py:150 +msgid "Tatar" +msgstr "" + +#: settings/common.py:151 +msgid "Udmurt" +msgstr "" + +#: settings/common.py:152 +msgid "Ukrainian" +msgstr "" + +#: settings/common.py:153 +msgid "Urdu" +msgstr "" + +#: settings/common.py:154 +msgid "Vietnamese" +msgstr "" + +#: settings/common.py:155 +msgid "Simplified Chinese" +msgstr "" + +#: settings/common.py:156 +msgid "Traditional Chinese" +msgstr "" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..8b986771 --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,353 @@ +# taiga-back.main +# Copyright (C) 2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"PO-Revision-Date: 2015-03-25 20:09+0100\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Taiga Dev Team \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: settings/common.py:73 +msgid "Afrikaans" +msgstr "" + +#: settings/common.py:74 +msgid "Arabic" +msgstr "" + +#: settings/common.py:75 +msgid "Asturian" +msgstr "" + +#: settings/common.py:76 +msgid "Azerbaijani" +msgstr "" + +#: settings/common.py:77 +msgid "Bulgarian" +msgstr "" + +#: settings/common.py:78 +msgid "Belarusian" +msgstr "" + +#: settings/common.py:79 +msgid "Bengali" +msgstr "" + +#: settings/common.py:80 +msgid "Breton" +msgstr "" + +#: settings/common.py:81 +msgid "Bosnian" +msgstr "" + +#: settings/common.py:82 +msgid "Catalan" +msgstr "" + +#: settings/common.py:83 +msgid "Czech" +msgstr "" + +#: settings/common.py:84 +msgid "Welsh" +msgstr "" + +#: settings/common.py:85 +msgid "Danish" +msgstr "" + +#: settings/common.py:86 +msgid "German" +msgstr "" + +#: settings/common.py:87 +msgid "Greek" +msgstr "" + +#: settings/common.py:88 +msgid "English" +msgstr "" + +#: settings/common.py:89 +msgid "Australian English" +msgstr "" + +#: settings/common.py:90 +msgid "British English" +msgstr "" + +#: settings/common.py:91 +msgid "Esperanto" +msgstr "" + +#: settings/common.py:92 +msgid "Spanish" +msgstr "" + +#: settings/common.py:93 +msgid "Argentinian Spanish" +msgstr "" + +#: settings/common.py:94 +msgid "Mexican Spanish" +msgstr "" + +#: settings/common.py:95 +msgid "Nicaraguan Spanish" +msgstr "" + +#: settings/common.py:96 +msgid "Venezuelan Spanish" +msgstr "" + +#: settings/common.py:97 +msgid "Estonian" +msgstr "" + +#: settings/common.py:98 +msgid "Basque" +msgstr "" + +#: settings/common.py:99 +msgid "Persian" +msgstr "" + +#: settings/common.py:100 +msgid "Finnish" +msgstr "" + +#: settings/common.py:101 +msgid "French" +msgstr "" + +#: settings/common.py:102 +msgid "Frisian" +msgstr "" + +#: settings/common.py:103 +msgid "Irish" +msgstr "" + +#: settings/common.py:104 +msgid "Galician" +msgstr "" + +#: settings/common.py:105 +msgid "Hebrew" +msgstr "" + +#: settings/common.py:106 +msgid "Hindi" +msgstr "" + +#: settings/common.py:107 +msgid "Croatian" +msgstr "" + +#: settings/common.py:108 +msgid "Hungarian" +msgstr "" + +#: settings/common.py:109 +msgid "Interlingua" +msgstr "" + +#: settings/common.py:110 +msgid "Indonesian" +msgstr "" + +#: settings/common.py:111 +msgid "Ido" +msgstr "" + +#: settings/common.py:112 +msgid "Icelandic" +msgstr "" + +#: settings/common.py:113 +msgid "Italian" +msgstr "" + +#: settings/common.py:114 +msgid "Japanese" +msgstr "" + +#: settings/common.py:115 +msgid "Georgian" +msgstr "" + +#: settings/common.py:116 +msgid "Kazakh" +msgstr "" + +#: settings/common.py:117 +msgid "Khmer" +msgstr "" + +#: settings/common.py:118 +msgid "Kannada" +msgstr "" + +#: settings/common.py:119 +msgid "Korean" +msgstr "" + +#: settings/common.py:120 +msgid "Luxembourgish" +msgstr "" + +#: settings/common.py:121 +msgid "Lithuanian" +msgstr "" + +#: settings/common.py:122 +msgid "Latvian" +msgstr "" + +#: settings/common.py:123 +msgid "Macedonian" +msgstr "" + +#: settings/common.py:124 +msgid "Malayalam" +msgstr "" + +#: settings/common.py:125 +msgid "Mongolian" +msgstr "" + +#: settings/common.py:126 +msgid "Marathi" +msgstr "" + +#: settings/common.py:127 +msgid "Burmese" +msgstr "" + +#: settings/common.py:128 +msgid "Norwegian Bokmal" +msgstr "" + +#: settings/common.py:129 +msgid "Nepali" +msgstr "" + +#: settings/common.py:130 +msgid "Dutch" +msgstr "" + +#: settings/common.py:131 +msgid "Norwegian Nynorsk" +msgstr "" + +#: settings/common.py:132 +msgid "Ossetic" +msgstr "" + +#: settings/common.py:133 +msgid "Punjabi" +msgstr "" + +#: settings/common.py:134 +msgid "Polish" +msgstr "" + +#: settings/common.py:135 +msgid "Portuguese" +msgstr "" + +#: settings/common.py:136 +msgid "Brazilian Portuguese" +msgstr "" + +#: settings/common.py:137 +msgid "Romanian" +msgstr "" + +#: settings/common.py:138 +msgid "Russian" +msgstr "" + +#: settings/common.py:139 +msgid "Slovak" +msgstr "" + +#: settings/common.py:140 +msgid "Slovenian" +msgstr "" + +#: settings/common.py:141 +msgid "Albanian" +msgstr "" + +#: settings/common.py:142 +msgid "Serbian" +msgstr "" + +#: settings/common.py:143 +msgid "Serbian Latin" +msgstr "" + +#: settings/common.py:144 +msgid "Swedish" +msgstr "" + +#: settings/common.py:145 +msgid "Swahili" +msgstr "" + +#: settings/common.py:146 +msgid "Tamil" +msgstr "" + +#: settings/common.py:147 +msgid "Telugu" +msgstr "" + +#: settings/common.py:148 +msgid "Thai" +msgstr "" + +#: settings/common.py:149 +msgid "Turkish" +msgstr "" + +#: settings/common.py:150 +msgid "Tatar" +msgstr "" + +#: settings/common.py:151 +msgid "Udmurt" +msgstr "" + +#: settings/common.py:152 +msgid "Ukrainian" +msgstr "" + +#: settings/common.py:153 +msgid "Urdu" +msgstr "" + +#: settings/common.py:154 +msgid "Vietnamese" +msgstr "" + +#: settings/common.py:155 +msgid "Simplified Chinese" +msgstr "" + +#: settings/common.py:156 +msgid "Traditional Chinese" +msgstr "" diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..1057e92e --- /dev/null +++ b/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,355 @@ +# taiga-back.main +# Copyright (C) 2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"PO-Revision-Date: 2015-03-24 18:08+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/taiga-back/" +"language/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: settings/common.py:73 +msgid "Afrikaans" +msgstr "" + +#: settings/common.py:74 +msgid "Arabic" +msgstr "" + +#: settings/common.py:75 +msgid "Asturian" +msgstr "" + +#: settings/common.py:76 +msgid "Azerbaijani" +msgstr "" + +#: settings/common.py:77 +msgid "Bulgarian" +msgstr "" + +#: settings/common.py:78 +msgid "Belarusian" +msgstr "" + +#: settings/common.py:79 +msgid "Bengali" +msgstr "" + +#: settings/common.py:80 +msgid "Breton" +msgstr "" + +#: settings/common.py:81 +msgid "Bosnian" +msgstr "" + +#: settings/common.py:82 +msgid "Catalan" +msgstr "" + +#: settings/common.py:83 +msgid "Czech" +msgstr "" + +#: settings/common.py:84 +msgid "Welsh" +msgstr "" + +#: settings/common.py:85 +msgid "Danish" +msgstr "" + +#: settings/common.py:86 +msgid "German" +msgstr "" + +#: settings/common.py:87 +msgid "Greek" +msgstr "" + +#: settings/common.py:88 +msgid "English" +msgstr "" + +#: settings/common.py:89 +msgid "Australian English" +msgstr "" + +#: settings/common.py:90 +msgid "British English" +msgstr "" + +#: settings/common.py:91 +msgid "Esperanto" +msgstr "" + +#: settings/common.py:92 +msgid "Spanish" +msgstr "" + +#: settings/common.py:93 +msgid "Argentinian Spanish" +msgstr "" + +#: settings/common.py:94 +msgid "Mexican Spanish" +msgstr "" + +#: settings/common.py:95 +msgid "Nicaraguan Spanish" +msgstr "" + +#: settings/common.py:96 +msgid "Venezuelan Spanish" +msgstr "" + +#: settings/common.py:97 +msgid "Estonian" +msgstr "" + +#: settings/common.py:98 +msgid "Basque" +msgstr "" + +#: settings/common.py:99 +msgid "Persian" +msgstr "" + +#: settings/common.py:100 +msgid "Finnish" +msgstr "" + +#: settings/common.py:101 +msgid "French" +msgstr "" + +#: settings/common.py:102 +msgid "Frisian" +msgstr "" + +#: settings/common.py:103 +msgid "Irish" +msgstr "" + +#: settings/common.py:104 +msgid "Galician" +msgstr "" + +#: settings/common.py:105 +msgid "Hebrew" +msgstr "" + +#: settings/common.py:106 +msgid "Hindi" +msgstr "" + +#: settings/common.py:107 +msgid "Croatian" +msgstr "" + +#: settings/common.py:108 +msgid "Hungarian" +msgstr "" + +#: settings/common.py:109 +msgid "Interlingua" +msgstr "" + +#: settings/common.py:110 +msgid "Indonesian" +msgstr "" + +#: settings/common.py:111 +msgid "Ido" +msgstr "" + +#: settings/common.py:112 +msgid "Icelandic" +msgstr "" + +#: settings/common.py:113 +msgid "Italian" +msgstr "" + +#: settings/common.py:114 +msgid "Japanese" +msgstr "" + +#: settings/common.py:115 +msgid "Georgian" +msgstr "" + +#: settings/common.py:116 +msgid "Kazakh" +msgstr "" + +#: settings/common.py:117 +msgid "Khmer" +msgstr "" + +#: settings/common.py:118 +msgid "Kannada" +msgstr "" + +#: settings/common.py:119 +msgid "Korean" +msgstr "" + +#: settings/common.py:120 +msgid "Luxembourgish" +msgstr "" + +#: settings/common.py:121 +msgid "Lithuanian" +msgstr "" + +#: settings/common.py:122 +msgid "Latvian" +msgstr "" + +#: settings/common.py:123 +msgid "Macedonian" +msgstr "" + +#: settings/common.py:124 +msgid "Malayalam" +msgstr "" + +#: settings/common.py:125 +msgid "Mongolian" +msgstr "" + +#: settings/common.py:126 +msgid "Marathi" +msgstr "" + +#: settings/common.py:127 +msgid "Burmese" +msgstr "" + +#: settings/common.py:128 +msgid "Norwegian Bokmal" +msgstr "" + +#: settings/common.py:129 +msgid "Nepali" +msgstr "" + +#: settings/common.py:130 +msgid "Dutch" +msgstr "" + +#: settings/common.py:131 +msgid "Norwegian Nynorsk" +msgstr "" + +#: settings/common.py:132 +msgid "Ossetic" +msgstr "" + +#: settings/common.py:133 +msgid "Punjabi" +msgstr "" + +#: settings/common.py:134 +msgid "Polish" +msgstr "" + +#: settings/common.py:135 +msgid "Portuguese" +msgstr "" + +#: settings/common.py:136 +msgid "Brazilian Portuguese" +msgstr "" + +#: settings/common.py:137 +msgid "Romanian" +msgstr "" + +#: settings/common.py:138 +msgid "Russian" +msgstr "" + +#: settings/common.py:139 +msgid "Slovak" +msgstr "" + +#: settings/common.py:140 +msgid "Slovenian" +msgstr "" + +#: settings/common.py:141 +msgid "Albanian" +msgstr "" + +#: settings/common.py:142 +msgid "Serbian" +msgstr "" + +#: settings/common.py:143 +msgid "Serbian Latin" +msgstr "" + +#: settings/common.py:144 +msgid "Swedish" +msgstr "" + +#: settings/common.py:145 +msgid "Swahili" +msgstr "" + +#: settings/common.py:146 +msgid "Tamil" +msgstr "" + +#: settings/common.py:147 +msgid "Telugu" +msgstr "" + +#: settings/common.py:148 +msgid "Thai" +msgstr "" + +#: settings/common.py:149 +msgid "Turkish" +msgstr "" + +#: settings/common.py:150 +msgid "Tatar" +msgstr "" + +#: settings/common.py:151 +msgid "Udmurt" +msgstr "" + +#: settings/common.py:152 +msgid "Ukrainian" +msgstr "" + +#: settings/common.py:153 +msgid "Urdu" +msgstr "" + +#: settings/common.py:154 +msgid "Vietnamese" +msgstr "" + +#: settings/common.py:155 +msgid "Simplified Chinese" +msgstr "" + +#: settings/common.py:156 +msgid "Traditional Chinese" +msgstr "" diff --git a/requirements-devel.txt b/requirements-devel.txt index 45c11b38..f9097069 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -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 diff --git a/scripts/manage_translations.py b/scripts/manage_translations.py new file mode 100644 index 00000000..8519a8eb --- /dev/null +++ b/scripts/manage_translations.py @@ -0,0 +1,246 @@ +#!/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, include_main=True): + """ + Return a tuple (app name, absolute path) for all locale directories, + optionally including the main catalog. + 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)) + if include_main: + dirs.insert(0, ("main", os.path.join(os.getcwd(), "locale"))) + + # 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 = + password = + """) + 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()))) diff --git a/settings/common.py b/settings/common.py index dc8d2b9d..b75a4e8c 100644 --- a/settings/common.py +++ b/settings/common.py @@ -15,7 +15,11 @@ # along with this program. If not, see . 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', gettext_noop('Afrikaans')), + ('ar', gettext_noop('Arabic')), + ('ast', gettext_noop('Asturian')), + ('az', gettext_noop('Azerbaijani')), + ('bg', gettext_noop('Bulgarian')), + ('be', gettext_noop('Belarusian')), + ('bn', gettext_noop('Bengali')), + ('br', gettext_noop('Breton')), + ('bs', gettext_noop('Bosnian')), + ('ca', gettext_noop('Catalan')), + ('cs', gettext_noop('Czech')), + ('cy', gettext_noop('Welsh')), + ('da', gettext_noop('Danish')), + ('de', gettext_noop('German')), + ('el', gettext_noop('Greek')), + ('en', gettext_noop('English')), + ('en-au', gettext_noop('Australian English')), + ('en-gb', gettext_noop('British English')), + ('eo', gettext_noop('Esperanto')), + ('es', gettext_noop('Spanish')), + ('es-ar', gettext_noop('Argentinian Spanish')), + ('es-mx', gettext_noop('Mexican Spanish')), + ('es-ni', gettext_noop('Nicaraguan Spanish')), + ('es-ve', gettext_noop('Venezuelan Spanish')), + ('et', gettext_noop('Estonian')), + ('eu', gettext_noop('Basque')), + ('fa', gettext_noop('Persian')), + ('fi', gettext_noop('Finnish')), + ('fr', gettext_noop('French')), + ('fy', gettext_noop('Frisian')), + ('ga', gettext_noop('Irish')), + ('gl', gettext_noop('Galician')), + ('he', gettext_noop('Hebrew')), + ('hi', gettext_noop('Hindi')), + ('hr', gettext_noop('Croatian')), + ('hu', gettext_noop('Hungarian')), + ('ia', gettext_noop('Interlingua')), + ('id', gettext_noop('Indonesian')), + ('io', gettext_noop('Ido')), + ('is', gettext_noop('Icelandic')), + ('it', gettext_noop('Italian')), + ('ja', gettext_noop('Japanese')), + ('ka', gettext_noop('Georgian')), + ('kk', gettext_noop('Kazakh')), + ('km', gettext_noop('Khmer')), + ('kn', gettext_noop('Kannada')), + ('ko', gettext_noop('Korean')), + ('lb', gettext_noop('Luxembourgish')), + ('lt', gettext_noop('Lithuanian')), + ('lv', gettext_noop('Latvian')), + ('mk', gettext_noop('Macedonian')), + ('ml', gettext_noop('Malayalam')), + ('mn', gettext_noop('Mongolian')), + ('mr', gettext_noop('Marathi')), + ('my', gettext_noop('Burmese')), + ('nb', gettext_noop('Norwegian Bokmal')), + ('ne', gettext_noop('Nepali')), + ('nl', gettext_noop('Dutch')), + ('nn', gettext_noop('Norwegian Nynorsk')), + ('os', gettext_noop('Ossetic')), + ('pa', gettext_noop('Punjabi')), + ('pl', gettext_noop('Polish')), + ('pt', gettext_noop('Portuguese')), + ('pt-br', gettext_noop('Brazilian Portuguese')), + ('ro', gettext_noop('Romanian')), + ('ru', gettext_noop('Russian')), + ('sk', gettext_noop('Slovak')), + ('sl', gettext_noop('Slovenian')), + ('sq', gettext_noop('Albanian')), + ('sr', gettext_noop('Serbian')), + ('sr-latn', gettext_noop('Serbian Latin')), + ('sv', gettext_noop('Swedish')), + ('sw', gettext_noop('Swahili')), + ('ta', gettext_noop('Tamil')), + ('te', gettext_noop('Telugu')), + ('th', gettext_noop('Thai')), + ('tr', gettext_noop('Turkish')), + ('tt', gettext_noop('Tatar')), + ('udm', gettext_noop('Udmurt')), + ('uk', gettext_noop('Ukrainian')), + ('ur', gettext_noop('Urdu')), + ('vi', gettext_noop('Vietnamese')), + ('zh-hans', gettext_noop('Simplified Chinese')), + ('zh-hant', gettext_noop('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"}, diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po new file mode 100644 index 00000000..6066e04f --- /dev/null +++ b/taiga/locale/ca/LC_MESSAGES/django.po @@ -0,0 +1,1589 @@ +# taiga-back.taiga. +# Copyright (C) 2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +# Javier Julián Olmos , 2015 +# Taiga Dev Team , 2015 +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"PO-Revision-Date: 2015-03-25 13:03+0000\n" +"Last-Translator: Javier Julián Olmos \n" +"Language-Team: Catalan (http://www.transifex.com/projects/p/taiga-back/" +"language/ca/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: taiga/auth/api.py:100 +msgid "Public register is disabled." +msgstr "El registre públic està deshabilitat" + +#: taiga/auth/api.py:133 +msgid "invalid register type" +msgstr "Sistema de registre invàlid" + +#: taiga/auth/api.py:146 +msgid "invalid login type" +msgstr "Sistema de login invàlid" + +#: taiga/auth/services.py:75 +msgid "Username is already in use." +msgstr "El mot d'usuari ja està en ús." + +#: taiga/auth/services.py:78 +msgid "Email is already in use." +msgstr "Aquest e-mail ja està en ús." + +#: taiga/auth/services.py:172 +msgid "Error on creating new user." +msgstr "Error creant un nou usuari." + +#: taiga/base/api/generics.py:162 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "La página no es 'last' ni pot ser convertida a un 'int'" + +#: taiga/base/api/generics.py:166 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Pàgina invàlida (%(page_number)s): %(message)s" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "Error de connexió." + +#: taiga/base/exceptions.py:30 +msgid "Unexpected error" +msgstr "Error inesperat." + +#: taiga/base/exceptions.py:42 +msgid "Not found." +msgstr "No s'ha trobat." + +#: taiga/base/exceptions.py:47 +msgid "Method not supported for this endpoint." +msgstr "Mètode no suportat per aquest endpoint." + +#: taiga/base/exceptions.py:55 taiga/base/exceptions.py:63 +msgid "Wrong arguments." +msgstr "Arguments invàlids." + +#: taiga/base/exceptions.py:67 +msgid "Data validation error" +msgstr "Validació de data errònia" + +#: taiga/base/exceptions.py:79 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "Error d'integritat per argument invàlid o erroni." + +#: taiga/base/exceptions.py:86 +msgid "Precondition error" +msgstr "Precondició errònia." + +#: taiga/base/tags.py:25 +msgid "tags" +msgstr "tags" + +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "Taiga" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "Segueix-nos a Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "Aconsegueix el codi a Github" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "Visita la nostra pàgina web" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "Taiga.io" + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Has sigut Taigatizat" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

\n" +" " +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "[Taiga] Actualitzacions" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "Actualitzacions" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

\n" +" " +msgstr "" +"\n" +"

Comentari:

\n" +"

%(comment)s

\n" +" " + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" +"\n" +" Comentari: %(comment)s\n" +" " + +#: taiga/export_import/api.py:183 +msgid "Needed dump file" +msgstr "Es necessita arxiu dump." + +#: taiga/export_import/api.py:190 +msgid "Invalid dump format" +msgstr "Format d'arxiu dump invàlid" + +#: taiga/export_import/serializers.py:377 +#: taiga/projects/custom_attributes/serializers.py:104 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}" + +#: taiga/export_import/serializers.py:392 +#: taiga/projects/custom_attributes/serializers.py:119 +msgid "It contain invalid custom fields." +msgstr "Conté camps personalitzats invàlids." + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "[%(project)s] El bolcat de dades del teu projecte ha sigut generat " + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "[%(project)s] %(error_subject)s" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "[Taiga] %(error_subject)s" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "[%(project)s] El teu bolcat de dades ha sigut importat" + +#: taiga/feedback/models.py:23 taiga/users/models.py:111 +msgid "full name" +msgstr "Nom complet" + +#: taiga/feedback/models.py:25 taiga/users/models.py:106 +msgid "email address" +msgstr "Adreça d'email" + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "Comentari" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:126 taiga/projects/models.py:549 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +msgid "created date" +msgstr "Data de creació" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

\n" +" " +msgstr "" +"\n" +"

Feedback

\n" +"

Taiga ha rebut feedback de %(full_name)s <%(email)s>

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

\n" +" " +msgstr "" +"\n" +"

Comentari

\n" +"

%(comment)s

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "Informació extra" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" +"---------\n" +"- De: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comentari:\n" +"%(comment)s\n" +"---------" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "- Informació extra:" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" +"\n" +"[Taiga] Feedback de %(full_name)s <%(email)s>\n" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "El payload no és un arxiu json vàlid" + +#: taiga/hooks/api.py:61 +msgid "The project doesn't exist" +msgstr "El projecte no existeix" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "Firma no vàlida." + +#: taiga/hooks/bitbucket/api.py:40 +msgid "The payload is not a valid application/x-www-form-urlencoded" +msgstr "El payload no és un application/x-www-form-urlencoded vàlid" + +#: taiga/hooks/bitbucket/event_hooks.py:46 +msgid "The payload is not valid" +msgstr "El payload no és vàlid" + +#: taiga/hooks/bitbucket/event_hooks.py:82 +#: taiga/hooks/github/event_hooks.py:75 taiga/hooks/gitlab/event_hooks.py:74 +msgid "The referenced element doesn't exist" +msgstr "L'element referenciat no existeix" + +#: taiga/hooks/bitbucket/event_hooks.py:89 +#: taiga/hooks/github/event_hooks.py:82 taiga/hooks/gitlab/event_hooks.py:81 +msgid "The status doesn't exist" +msgstr "L'estatus no existeix." + +#: taiga/hooks/github/event_hooks.py:113 taiga/hooks/gitlab/event_hooks.py:114 +msgid "Invalid issue information" +msgstr "Informació d'incidència no vàlida." + +#: taiga/hooks/github/event_hooks.py:135 taiga/hooks/github/event_hooks.py:144 +msgid "Invalid issue comment information" +msgstr "Informació del comentari a l'incidència no vàlid." + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:52 +msgid "View project" +msgstr "Veure projecte" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:54 +msgid "View milestones" +msgstr "Veure fita" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "Veure història d'usuari" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:64 +msgid "View tasks" +msgstr "Veure tasca" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:69 +msgid "View issues" +msgstr "Veure incidència" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:75 +msgid "View wiki pages" +msgstr "Veure pàgina del wiki" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 +#: taiga/permissions/permissions.py:80 +msgid "View wiki links" +msgstr "Veure links del wiki" + +#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 +msgid "Vote issues" +msgstr "Vota incidéncies" + +#: taiga/permissions/permissions.py:39 +msgid "Request membership" +msgstr "Demana membresía" + +#: taiga/permissions/permissions.py:40 +msgid "Add user story to project" +msgstr "Afegeix història d'usuari a projecte" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to user stories" +msgstr "Afegeix comentaris a històries d'usuari" + +#: taiga/permissions/permissions.py:42 +msgid "Add comments to tasks" +msgstr "Afegeix comentaris a tasques" + +#: taiga/permissions/permissions.py:43 +msgid "Add issues" +msgstr "Afegeix incidéncies" + +#: taiga/permissions/permissions.py:44 +msgid "Add comments to issues" +msgstr "Afegeix comentaris a incidéncies" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +msgid "Add wiki page" +msgstr "Afegeix pàgina del wiki" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +msgid "Modify wiki page" +msgstr "Modifica pàgina del wiki" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +msgid "Add wiki link" +msgstr "Afegeix enllaç de wiki" + +#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +msgid "Modify wiki link" +msgstr "Modifica enllaç de wiki" + +#: taiga/permissions/permissions.py:55 +msgid "Add milestone" +msgstr "Afegeix fita" + +#: taiga/permissions/permissions.py:56 +msgid "Modify milestone" +msgstr "Modifica fita" + +#: taiga/permissions/permissions.py:57 +msgid "Delete milestone" +msgstr "Borra fita" + +#: taiga/permissions/permissions.py:59 +msgid "View user story" +msgstr "Veure història d'usuari" + +#: taiga/permissions/permissions.py:60 +msgid "Add user story" +msgstr "Afegeix història d'usuari" + +#: taiga/permissions/permissions.py:61 +msgid "Modify user story" +msgstr "Modifica història d'usuari" + +#: taiga/permissions/permissions.py:62 +msgid "Delete user story" +msgstr "Borra història d'usuari" + +#: taiga/permissions/permissions.py:65 +msgid "Add task" +msgstr "Afegeix tasca" + +#: taiga/permissions/permissions.py:66 +msgid "Modify task" +msgstr "Modifica tasca" + +#: taiga/permissions/permissions.py:67 +msgid "Delete task" +msgstr "Borra tasca" + +#: taiga/permissions/permissions.py:71 +msgid "Add issue" +msgstr "Afegeix incidència" + +#: taiga/permissions/permissions.py:72 +msgid "Modify issue" +msgstr "Modifica incidència" + +#: taiga/permissions/permissions.py:73 +msgid "Delete issue" +msgstr "Borra incidència" + +#: taiga/permissions/permissions.py:78 +msgid "Delete wiki page" +msgstr "Borra pàgina de wiki" + +#: taiga/permissions/permissions.py:83 +msgid "Delete wiki link" +msgstr "Borra enllaç de wiki" + +#: taiga/permissions/permissions.py:87 +msgid "Modify project" +msgstr "Modifica projecte" + +#: taiga/permissions/permissions.py:88 +msgid "Add member" +msgstr "Afegeix membre" + +#: taiga/permissions/permissions.py:89 +msgid "Remove member" +msgstr "Borra membre" + +#: taiga/permissions/permissions.py:90 +msgid "Delete project" +msgstr "Borra projecte" + +#: taiga/permissions/permissions.py:91 +msgid "Admin project values" +msgstr "Administrar valors de projecte" + +#: taiga/permissions/permissions.py:92 +msgid "Admin roles" +msgstr "Administrar rols" + +#: taiga/projects/api.py:454 taiga/projects/serializers.py:227 +msgid "At least one of the user must be an active admin" +msgstr "Al menys un del usuaris ha de ser administrador" + +#: taiga/projects/api.py:484 +msgid "You don't have permisions to see that." +msgstr "No tens permisos per a veure açò." + +#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:131 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:64 +#: taiga/projects/wiki/models.py:34 taiga/userstorage/models.py:25 +msgid "owner" +msgstr "Amo" + +#: taiga/projects/attachments/models.py:56 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:326 taiga/projects/models.py:352 +#: taiga/projects/models.py:383 taiga/projects/models.py:412 +#: taiga/projects/models.py:445 taiga/projects/models.py:468 +#: taiga/projects/models.py:495 taiga/projects/models.py:526 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:62 +#: taiga/projects/wiki/models.py:28 taiga/projects/wiki/models.py:66 +#: taiga/users/models.py:193 +msgid "project" +msgstr "Projecte" + +#: taiga/projects/attachments/models.py:58 +msgid "content type" +msgstr "Tipus de contingut" + +#: taiga/projects/attachments/models.py:60 +msgid "object id" +msgstr "Id d'objecte" + +#: taiga/projects/attachments/models.py:66 +#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:129 taiga/projects/models.py:552 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "Data de modificació" + +#: taiga/projects/attachments/models.py:71 +msgid "attached file" +msgstr "Arxiu adjunt" + +#: taiga/projects/attachments/models.py:74 +msgid "is deprecated" +msgstr "està obsolet " + +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:32 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:124 +#: taiga/projects/models.py:547 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "Descripció" + +#: taiga/projects/attachments/models.py:76 +#: taiga/projects/custom_attributes/models.py:33 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:342 +#: taiga/projects/models.py:379 taiga/projects/models.py:406 +#: taiga/projects/models.py:441 taiga/projects/models.py:464 +#: taiga/projects/models.py:489 taiga/projects/models.py:522 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:188 +msgid "order" +msgstr "Ordre" + +#: taiga/projects/custom_attributes/models.py:31 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:120 +#: taiga/projects/models.py:338 taiga/projects/models.py:377 +#: taiga/projects/models.py:402 taiga/projects/models.py:439 +#: taiga/projects/models.py:462 taiga/projects/models.py:485 +#: taiga/projects/models.py:520 taiga/projects/models.py:543 +#: taiga/users/models.py:180 taiga/webhooks/models.py:27 +msgid "name" +msgstr "Nom" + +#: taiga/projects/custom_attributes/models.py:81 +msgid "attributes_values" +msgstr "valors_atributs" + +#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 +msgid "user story" +msgstr "història d'usuari" + +#: taiga/projects/custom_attributes/models.py:106 +msgid "task" +msgstr "tasca" + +#: taiga/projects/custom_attributes/models.py:121 +msgid "issue" +msgstr "incidéncia" + +#: taiga/projects/custom_attributes/serializers.py:58 +msgid "Already exists one with the same name." +msgstr "Ja existix altre amb el matex nom." + +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "Canvia" + +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "Crea" + +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "Borra" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "%(role)s punts de rol" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:192 +msgid "from" +msgstr "De" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:198 +msgid "to" +msgstr "a" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "Afegir nou arxiu" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "Arxiu actualitzat" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "Obsolet" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "No obsolet" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "Arxiu borrat" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "Afegit" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "Borrat" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +msgid "Unassigned" +msgstr "Sense assignar" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:209 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "-borrat-" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "a:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "desde:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "Afegit" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "Canviat" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "Borrat" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "afegit:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "borrat:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "Desde:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "A:" + +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:32 +msgid "content" +msgstr "contingut" + +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "nota de bloqueig" + +#: taiga/projects/issues/api.py:139 +msgid "You don't have permissions to set this sprint to this issue." +msgstr "No tens permissos per a ficar aquest sprint a aquesta incidència" + +#: taiga/projects/issues/api.py:143 +msgid "You don't have permissions to set this status to this issue." +msgstr "No tens permissos per a ficar aquest status a aquesta tasca" + +#: taiga/projects/issues/api.py:147 +msgid "You don't have permissions to set this severity to this issue." +msgstr "No tens permissos per a ficar aquesta severitat a aquesta tasca" + +#: taiga/projects/issues/api.py:151 +msgid "You don't have permissions to set this priority to this issue." +msgstr "No tens permissos per a ficar aquesta prioritat a aquesta incidència" + +#: taiga/projects/issues/api.py:155 +msgid "You don't have permissions to set this type to this issue." +msgstr "No tens permissos per a ficar aquest tipus a aquesta incidència" + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "ref" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "estatus" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "severitat" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "prioritat" + +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "tipus" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "fita" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "Data de finalització" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "tema" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "assignada a" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "referència externa" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:122 +#: taiga/projects/models.py:340 taiga/projects/models.py:404 +#: taiga/projects/models.py:487 taiga/projects/models.py:545 +#: taiga/projects/wiki/models.py:30 taiga/users/models.py:182 +msgid "slug" +msgstr "slug" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "Data estimada d'inici" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "Data estimada de finalització" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:344 +#: taiga/projects/models.py:408 taiga/projects/models.py:491 +msgid "is closed" +msgstr "està tancat" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "disponibilitat" + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "No hi ha cap sprint amb aquest id" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "està bloquejat" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "{param} parameter is mandatory" +msgstr "{param} paràmetre és obligatori" + +#: taiga/projects/mixins/ordering.py:51 +msgid "project parameter is mandatory" +msgstr "el paràmetre de projecte és obligatori" + +#: taiga/projects/models.py:59 +msgid "email" +msgstr "email" + +#: taiga/projects/models.py:61 +msgid "creado el" +msgstr "creat el" + +#: taiga/projects/models.py:63 taiga/users/models.py:126 +msgid "token" +msgstr "token" + +#: taiga/projects/models.py:69 +msgid "invitation extra text" +msgstr "text extra d'invitació" + +#: taiga/projects/models.py:75 +msgid "The user is already member of the project" +msgstr "L'usuari ja es membre del projecte" + +#: taiga/projects/models.py:90 +msgid "default points" +msgstr "Points per defecte" + +#: taiga/projects/models.py:94 +msgid "default US status" +msgstr "estatus d'història d'usuai per defecte" + +#: taiga/projects/models.py:98 +msgid "default task status" +msgstr "Estatus de tasca per defecte" + +#: taiga/projects/models.py:101 +msgid "default priority" +msgstr "Prioritat per defecte" + +#: taiga/projects/models.py:104 +msgid "default severity" +msgstr "Severitat per defecte" + +#: taiga/projects/models.py:108 +msgid "default issue status" +msgstr "Status d'incidència per defecte" + +#: taiga/projects/models.py:112 +msgid "default issue type" +msgstr "Tipus d'incidència per defecte" + +#: taiga/projects/models.py:133 +msgid "members" +msgstr "membres" + +#: taiga/projects/models.py:136 +msgid "total of milestones" +msgstr "total de fites" + +#: taiga/projects/models.py:137 +msgid "total story points" +msgstr "total de punts d'història" + +#: taiga/projects/models.py:140 taiga/projects/models.py:558 +msgid "active backlog panel" +msgstr "activa panell de backlog" + +#: taiga/projects/models.py:142 taiga/projects/models.py:560 +msgid "active kanban panel" +msgstr "activa panell de kanban" + +#: taiga/projects/models.py:144 taiga/projects/models.py:562 +msgid "active wiki panel" +msgstr "activa panell de wiki" + +#: taiga/projects/models.py:146 taiga/projects/models.py:564 +msgid "active issues panel" +msgstr "activa panell d'incidències" + +#: taiga/projects/models.py:149 taiga/projects/models.py:567 +msgid "videoconference system" +msgstr "sistema de videoconferència" + +#: taiga/projects/models.py:151 taiga/projects/models.py:569 +msgid "videoconference room salt" +msgstr "sala videoconferència" + +#: taiga/projects/models.py:156 +msgid "creation template" +msgstr "template de creació" + +#: taiga/projects/models.py:159 +msgid "anonymous permissions" +msgstr "permisos d'anònims" + +#: taiga/projects/models.py:163 +msgid "user permissions" +msgstr "permisos d'usuaris" + +#: taiga/projects/models.py:166 +msgid "is private" +msgstr "es privat" + +#: taiga/projects/models.py:177 +msgid "tags colors" +msgstr "colors de tags" + +#: taiga/projects/models.py:327 +msgid "modules config" +msgstr "configuració de mòdules" + +#: taiga/projects/models.py:346 +msgid "is archived" +msgstr "està arxivat" + +#: taiga/projects/models.py:348 taiga/projects/models.py:410 +#: taiga/projects/models.py:443 taiga/projects/models.py:466 +#: taiga/projects/models.py:493 taiga/projects/models.py:524 +#: taiga/users/models.py:113 +msgid "color" +msgstr "color" + +#: taiga/projects/models.py:350 +msgid "work in progress limit" +msgstr "limit de treball en progrés" + +#: taiga/projects/models.py:381 taiga/userstorage/models.py:31 +msgid "value" +msgstr "valor" + +#: taiga/projects/models.py:555 +msgid "default owner's role" +msgstr "rol d'amo per defecte" + +#: taiga/projects/models.py:571 +msgid "default options" +msgstr "opcions per defecte" + +#: taiga/projects/models.py:572 +msgid "us statuses" +msgstr "status d'històries d'usuari" + +#: taiga/projects/models.py:573 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 +msgid "points" +msgstr "punts" + +#: taiga/projects/models.py:574 +msgid "task statuses" +msgstr "status de tasques" + +#: taiga/projects/models.py:575 +msgid "issue statuses" +msgstr "status d'incidències" + +#: taiga/projects/models.py:576 +msgid "issue types" +msgstr "tipus d'incidències" + +#: taiga/projects/models.py:577 +msgid "priorities" +msgstr "prioritats" + +#: taiga/projects/models.py:578 +msgid "severities" +msgstr "severitats" + +#: taiga/projects/models.py:579 +msgid "roles" +msgstr "rols" + +#: taiga/projects/notifications/choices.py:28 +msgid "Not watching" +msgstr "No observant" + +#: taiga/projects/notifications/choices.py:29 +msgid "Watching" +msgstr "Observant" + +#: taiga/projects/notifications/choices.py:30 +msgid "Ignoring" +msgstr "Ignorant" + +#: taiga/projects/notifications/mixins.py:87 +msgid "watchers" +msgstr "Observadors" + +#: taiga/projects/notifications/models.py:57 +msgid "created date time" +msgstr "creada data" + +#: taiga/projects/notifications/models.py:59 +msgid "updated date time" +msgstr "Actualitzada data" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Actualizada pàgina de Wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Creada pàgina de Wiki \"%(page)s\"\n" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" +"\n" +"[%(project)s] Borrada pàgina de Wiki \"%(page)s\"\n" + +#: taiga/projects/occ/mixins.py:91 +msgid "version" +msgstr "Versió" + +#: taiga/projects/permissions.py:39 +msgid "You can't leave the project if there are no more owners" +msgstr "No pots deixar el projecte si no hi ha més amos" + +#: taiga/projects/serializers.py:203 +msgid "Email address is already taken" +msgstr "Aquest e-mail ja està en ús" + +#: taiga/projects/serializers.py:215 +msgid "Invalid role for the project" +msgstr "Rol invàlid per al projecte" + +#: taiga/projects/serializers.py:370 +msgid "Default options" +msgstr "Opcions per defecte" + +#: taiga/projects/serializers.py:371 +msgid "User story's statuses" +msgstr "Estatus d'històries d'usuari" + +#: taiga/projects/serializers.py:372 +msgid "Points" +msgstr "Punts" + +#: taiga/projects/serializers.py:373 +msgid "Task's statuses" +msgstr "Estatus de tasques" + +#: taiga/projects/serializers.py:374 +msgid "Issue's statuses" +msgstr "Estatus d'incidéncies" + +#: taiga/projects/serializers.py:375 +msgid "Issue's types" +msgstr "Tipus d'incidéncies" + +#: taiga/projects/serializers.py:376 +msgid "Priorities" +msgstr "Prioritats" + +#: taiga/projects/serializers.py:377 +msgid "Severities" +msgstr "Severitats" + +#: taiga/projects/serializers.py:378 +msgid "Roles" +msgstr "Rols" + +#: taiga/projects/tasks/api.py:57 taiga/projects/tasks/api.py:60 +#: taiga/projects/tasks/api.py:63 taiga/projects/tasks/api.py:66 +msgid "You don't have permissions for add/modify this task." +msgstr "No tens permissos per a afegir/modificar aquesta tasca." + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "order d'històries d'usuari" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "ordre de taskboard" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "es iocaina" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "No hi ha cap tasca amb eixe id" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "algú" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:18 +#, python-format +msgid "" +"\n" +"

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

\n" +" " +msgstr "" +"\n" +"

I ara unes poques paraules del bon company o companya
que ha " +"tingut el bon pensament de convidar-te

\n" +"

%(extra)s

\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "Accept your invitation to Taiga" +msgstr "Acepta la invitació a Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "Accept your invitation" +msgstr "Acepta la invitació" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:26 +msgid "The Taiga Team" +msgstr "El equip de Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" +"\n" +"I ara unes poques paraules del bon company o companya que ha tingut el bon " +"pensament de convidar-te\n" +"\n" +"%(extra)s\n" +" " + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:19 +msgid "Accept your invitation to Taiga following whis link:" +msgstr "Acepta la invitació a Taiga fent click a aquest link:" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:21 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" +"\n" +"---\n" +"El equip de Taiga\n" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Invitació de Taiga per al projecte '%(project)s'\n" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" +"\n" +"[Taiga] Afegit al projecte '%(project)s'\n" + +#: taiga/projects/userstories/api.py:170 +#, python-brace-format +msgid "" +"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " +"{subject}\")" +msgstr "" +"Generant l'història d'usuari [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " +"{subject}\")" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "rol" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "ordre de backlog" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "ordre d'sprint" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "data de finalització" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "requeriment de client" + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "requeriment d'equip" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "generat desde incidéncia" + +#: taiga/projects/userstories/validators.py:12 +msgid "There's no user story with that id" +msgstr "No hi ha cap història d'usuari amb eixe id" + +#: taiga/projects/validators.py:12 +msgid "There's no project with that id" +msgstr "No hi ha cap projecte amb eixe id" + +#: taiga/projects/validators.py:21 +msgid "There's no user story status with that id" +msgstr "No hi ha cap estatis d'història d'usuari amb eixe id" + +#: taiga/projects/validators.py:30 +msgid "There's no task status with that id" +msgstr "No hi ha cap estatus de tasca amb eixe id" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:54 +msgid "Votes" +msgstr "Vots" + +#: taiga/projects/votes/models.py:50 +msgid "votes" +msgstr "vots" + +#: taiga/projects/votes/models.py:53 +msgid "Vote" +msgstr "Vot" + +#: taiga/projects/wiki/api.py:60 +msgid "No content parameter" +msgstr "No hi ha parametre de contingut" + +#: taiga/projects/wiki/api.py:63 +msgid "No project_id parameter" +msgstr "No hi ha parametre de project_id" + +#: taiga/projects/wiki/models.py:36 +msgid "last modifier" +msgstr "últim a modificar" + +#: taiga/projects/wiki/models.py:69 +msgid "href" +msgstr "href" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "Informació personal" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "Permissos" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "Dates importants" + +#: taiga/users/api.py:87 taiga/users/api.py:94 +msgid "Invalid username or email" +msgstr "Nom d'usuari o email invàlid" + +#: taiga/users/api.py:103 +msgid "Mail sended successful!" +msgstr "Correu enviat satisfactòriament" + +#: taiga/users/api.py:116 taiga/users/api.py:121 +msgid "Token is invalid" +msgstr "Token invàlid" + +#: taiga/users/api.py:142 +msgid "Current password parameter needed" +msgstr "Paràmetre de password actual requerit" + +#: taiga/users/api.py:145 +msgid "New password parameter needed" +msgstr "Paràmetre de password requerit" + +#: taiga/users/api.py:148 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Password invàlid, al menys 6 caràcters requerits" + +#: taiga/users/api.py:151 +msgid "Invalid current password" +msgstr "Password actual invàlid" + +#: taiga/users/api.py:167 +msgid "Incomplete arguments" +msgstr "Arguments incomplets." + +#: taiga/users/api.py:172 +msgid "Invalid image format" +msgstr "Format d'image invàlid" + +#: taiga/users/api.py:225 +msgid "Duplicated email" +msgstr "Email duplicat" + +#: taiga/users/api.py:227 +msgid "Not valid email" +msgstr "Email no vàlid" + +#: taiga/users/api.py:246 taiga/users/api.py:252 +msgid "" +"Invalid, are you sure the token is correct and you didn't use it before?" +msgstr "" +"Invàlid. Estás segur que el token es correcte i que no l'has usat abans?" + +#: taiga/users/api.py:279 taiga/users/api.py:287 taiga/users/api.py:290 +msgid "Invalid, are you sure the token is correct?" +msgstr "Invàlid. Estás segur que el token es correcte?" + +#: taiga/users/models.py:69 +msgid "superuser status" +msgstr "estatus de superusuari" + +#: taiga/users/models.py:70 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" +"Designa que aquest usuari te tots els permisos sense asignarli-los " +"explícitament." + +#: taiga/users/models.py:100 +msgid "username" +msgstr "mot d'usuari" + +#: taiga/users/models.py:101 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" +msgstr "Requerit. 30 caràcters o menys. Lletres, nombres i caràcters /./-/_" + +#: taiga/users/models.py:104 +msgid "Enter a valid username." +msgstr "Introdueix un nom d'usuari vàlid" + +#: taiga/users/models.py:107 +msgid "active" +msgstr "actiu" + +#: taiga/users/models.py:108 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" +"Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en " +"lloc de borrar el compte." + +#: taiga/users/models.py:114 +msgid "biography" +msgstr "biografia" + +#: taiga/users/models.py:117 +msgid "photo" +msgstr "foto" + +#: taiga/users/models.py:118 +msgid "date joined" +msgstr "data d'unió" + +#: taiga/users/models.py:120 +msgid "default language" +msgstr "llenguatge per defecte" + +#: taiga/users/models.py:122 +msgid "default timezone" +msgstr "zona horaria per defecte" + +#: taiga/users/models.py:124 +msgid "colorize tags" +msgstr "coloritza tags" + +#: taiga/users/models.py:129 +msgid "email token" +msgstr "token de correu" + +#: taiga/users/models.py:131 +msgid "new email address" +msgstr "nova adreça de correu" + +#: taiga/users/models.py:185 +msgid "permissions" +msgstr "permissos" + +#: taiga/users/serializers.py:52 +msgid "invalid username" +msgstr "nom d'usuari invàlid" + +#: taiga/users/serializers.py:53 +msgid "invalid" +msgstr "invàlid" + +#: taiga/users/serializers.py:58 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "Requerit. 255 caràcters o menys. Lletres, nombres i caràcters /./-/_" + +#: taiga/users/serializers.py:64 +msgid "Invalid username. Try with a different one." +msgstr "Nom d'usuari invàlid" + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "" + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "" + +#: taiga/webhooks/models.py:39 +msgid "Status code" +msgstr "" + +#: taiga/webhooks/models.py:40 +msgid "Request data" +msgstr "" + +#: taiga/webhooks/models.py:41 +msgid "Request headers" +msgstr "" + +#: taiga/webhooks/models.py:42 +msgid "Response data" +msgstr "" + +#: taiga/webhooks/models.py:43 +msgid "Response headers" +msgstr "" + +#: taiga/webhooks/models.py:44 +msgid "Duration" +msgstr "" diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..742c8bcd --- /dev/null +++ b/taiga/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,1531 @@ +# taiga-back.taiga. +# Copyright (C) 2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"PO-Revision-Date: 2015-03-25 20:09+0100\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Taiga Dev Team \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: taiga/auth/api.py:100 +msgid "Public register is disabled." +msgstr "" + +#: taiga/auth/api.py:133 +msgid "invalid register type" +msgstr "" + +#: taiga/auth/api.py:146 +msgid "invalid login type" +msgstr "" + +#: taiga/auth/services.py:75 +msgid "Username is already in use." +msgstr "" + +#: taiga/auth/services.py:78 +msgid "Email is already in use." +msgstr "" + +#: taiga/auth/services.py:172 +msgid "Error on creating new user." +msgstr "" + +#: taiga/base/api/generics.py:162 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "" + +#: taiga/base/api/generics.py:166 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "" + +#: taiga/base/exceptions.py:30 +msgid "Unexpected error" +msgstr "" + +#: taiga/base/exceptions.py:42 +msgid "Not found." +msgstr "" + +#: taiga/base/exceptions.py:47 +msgid "Method not supported for this endpoint." +msgstr "" + +#: taiga/base/exceptions.py:55 taiga/base/exceptions.py:63 +msgid "Wrong arguments." +msgstr "" + +#: taiga/base/exceptions.py:67 +msgid "Data validation error" +msgstr "" + +#: taiga/base/exceptions.py:79 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "" + +#: taiga/base/exceptions.py:86 +msgid "Precondition error" +msgstr "" + +#: taiga/base/tags.py:25 +msgid "tags" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "" + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

\n" +" " +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

\n" +" " +msgstr "" + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" + +#: taiga/export_import/api.py:183 +msgid "Needed dump file" +msgstr "" + +#: taiga/export_import/api.py:190 +msgid "Invalid dump format" +msgstr "" + +#: taiga/export_import/serializers.py:377 +#: taiga/projects/custom_attributes/serializers.py:104 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "" + +#: taiga/export_import/serializers.py:392 +#: taiga/projects/custom_attributes/serializers.py:119 +msgid "It contain invalid custom fields." +msgstr "" + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "" + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:111 +msgid "full name" +msgstr "" + +#: taiga/feedback/models.py:25 taiga/users/models.py:106 +msgid "email address" +msgstr "" + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:126 taiga/projects/models.py:549 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +msgid "created date" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

\n" +" " +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

\n" +" " +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "" + +#: taiga/hooks/api.py:61 +msgid "The project doesn't exist" +msgstr "" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "" + +#: taiga/hooks/bitbucket/api.py:40 +msgid "The payload is not a valid application/x-www-form-urlencoded" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:46 +msgid "The payload is not valid" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:82 +#: taiga/hooks/github/event_hooks.py:75 taiga/hooks/gitlab/event_hooks.py:74 +msgid "The referenced element doesn't exist" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:89 +#: taiga/hooks/github/event_hooks.py:82 taiga/hooks/gitlab/event_hooks.py:81 +msgid "The status doesn't exist" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:113 taiga/hooks/gitlab/event_hooks.py:114 +msgid "Invalid issue information" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:135 taiga/hooks/github/event_hooks.py:144 +msgid "Invalid issue comment information" +msgstr "" + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:52 +msgid "View project" +msgstr "" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:54 +msgid "View milestones" +msgstr "" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:64 +msgid "View tasks" +msgstr "" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:69 +msgid "View issues" +msgstr "" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:75 +msgid "View wiki pages" +msgstr "" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 +#: taiga/permissions/permissions.py:80 +msgid "View wiki links" +msgstr "" + +#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 +msgid "Vote issues" +msgstr "" + +#: taiga/permissions/permissions.py:39 +msgid "Request membership" +msgstr "" + +#: taiga/permissions/permissions.py:40 +msgid "Add user story to project" +msgstr "" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to user stories" +msgstr "" + +#: taiga/permissions/permissions.py:42 +msgid "Add comments to tasks" +msgstr "" + +#: taiga/permissions/permissions.py:43 +msgid "Add issues" +msgstr "" + +#: taiga/permissions/permissions.py:44 +msgid "Add comments to issues" +msgstr "" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +msgid "Add wiki page" +msgstr "" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +msgid "Modify wiki page" +msgstr "" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +msgid "Add wiki link" +msgstr "" + +#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +msgid "Modify wiki link" +msgstr "" + +#: taiga/permissions/permissions.py:55 +msgid "Add milestone" +msgstr "" + +#: taiga/permissions/permissions.py:56 +msgid "Modify milestone" +msgstr "" + +#: taiga/permissions/permissions.py:57 +msgid "Delete milestone" +msgstr "" + +#: taiga/permissions/permissions.py:59 +msgid "View user story" +msgstr "" + +#: taiga/permissions/permissions.py:60 +msgid "Add user story" +msgstr "" + +#: taiga/permissions/permissions.py:61 +msgid "Modify user story" +msgstr "" + +#: taiga/permissions/permissions.py:62 +msgid "Delete user story" +msgstr "" + +#: taiga/permissions/permissions.py:65 +msgid "Add task" +msgstr "" + +#: taiga/permissions/permissions.py:66 +msgid "Modify task" +msgstr "" + +#: taiga/permissions/permissions.py:67 +msgid "Delete task" +msgstr "" + +#: taiga/permissions/permissions.py:71 +msgid "Add issue" +msgstr "" + +#: taiga/permissions/permissions.py:72 +msgid "Modify issue" +msgstr "" + +#: taiga/permissions/permissions.py:73 +msgid "Delete issue" +msgstr "" + +#: taiga/permissions/permissions.py:78 +msgid "Delete wiki page" +msgstr "" + +#: taiga/permissions/permissions.py:83 +msgid "Delete wiki link" +msgstr "" + +#: taiga/permissions/permissions.py:87 +msgid "Modify project" +msgstr "" + +#: taiga/permissions/permissions.py:88 +msgid "Add member" +msgstr "" + +#: taiga/permissions/permissions.py:89 +msgid "Remove member" +msgstr "" + +#: taiga/permissions/permissions.py:90 +msgid "Delete project" +msgstr "" + +#: taiga/permissions/permissions.py:91 +msgid "Admin project values" +msgstr "" + +#: taiga/permissions/permissions.py:92 +msgid "Admin roles" +msgstr "" + +#: taiga/projects/api.py:454 taiga/projects/serializers.py:227 +msgid "At least one of the user must be an active admin" +msgstr "" + +#: taiga/projects/api.py:484 +msgid "You don't have permisions to see that." +msgstr "" + +#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:131 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:64 +#: taiga/projects/wiki/models.py:34 taiga/userstorage/models.py:25 +msgid "owner" +msgstr "" + +#: taiga/projects/attachments/models.py:56 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:326 taiga/projects/models.py:352 +#: taiga/projects/models.py:383 taiga/projects/models.py:412 +#: taiga/projects/models.py:445 taiga/projects/models.py:468 +#: taiga/projects/models.py:495 taiga/projects/models.py:526 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:62 +#: taiga/projects/wiki/models.py:28 taiga/projects/wiki/models.py:66 +#: taiga/users/models.py:193 +msgid "project" +msgstr "" + +#: taiga/projects/attachments/models.py:58 +msgid "content type" +msgstr "" + +#: taiga/projects/attachments/models.py:60 +msgid "object id" +msgstr "" + +#: taiga/projects/attachments/models.py:66 +#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:129 taiga/projects/models.py:552 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "" + +#: taiga/projects/attachments/models.py:71 +msgid "attached file" +msgstr "" + +#: taiga/projects/attachments/models.py:74 +msgid "is deprecated" +msgstr "" + +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:32 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:124 +#: taiga/projects/models.py:547 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "" + +#: taiga/projects/attachments/models.py:76 +#: taiga/projects/custom_attributes/models.py:33 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:342 +#: taiga/projects/models.py:379 taiga/projects/models.py:406 +#: taiga/projects/models.py:441 taiga/projects/models.py:464 +#: taiga/projects/models.py:489 taiga/projects/models.py:522 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:188 +msgid "order" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:31 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:120 +#: taiga/projects/models.py:338 taiga/projects/models.py:377 +#: taiga/projects/models.py:402 taiga/projects/models.py:439 +#: taiga/projects/models.py:462 taiga/projects/models.py:485 +#: taiga/projects/models.py:520 taiga/projects/models.py:543 +#: taiga/users/models.py:180 taiga/webhooks/models.py:27 +msgid "name" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:81 +msgid "attributes_values" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 +msgid "user story" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:106 +msgid "task" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:121 +msgid "issue" +msgstr "" + +#: taiga/projects/custom_attributes/serializers.py:58 +msgid "Already exists one with the same name." +msgstr "" + +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "" + +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "" + +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:192 +msgid "from" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:198 +msgid "to" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +msgid "Unassigned" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:209 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "" + +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:32 +msgid "content" +msgstr "" + +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "" + +#: taiga/projects/issues/api.py:139 +msgid "You don't have permissions to set this sprint to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:143 +msgid "You don't have permissions to set this status to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:147 +msgid "You don't have permissions to set this severity to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:151 +msgid "You don't have permissions to set this priority to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:155 +msgid "You don't have permissions to set this type to this issue." +msgstr "" + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "" + +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:122 +#: taiga/projects/models.py:340 taiga/projects/models.py:404 +#: taiga/projects/models.py:487 taiga/projects/models.py:545 +#: taiga/projects/wiki/models.py:30 taiga/users/models.py:182 +msgid "slug" +msgstr "" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:344 +#: taiga/projects/models.py:408 taiga/projects/models.py:491 +msgid "is closed" +msgstr "" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "" + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "{param} parameter is mandatory" +msgstr "" + +#: taiga/projects/mixins/ordering.py:51 +msgid "project parameter is mandatory" +msgstr "" + +#: taiga/projects/models.py:59 +msgid "email" +msgstr "" + +#: taiga/projects/models.py:61 +msgid "creado el" +msgstr "" + +#: taiga/projects/models.py:63 taiga/users/models.py:126 +msgid "token" +msgstr "" + +#: taiga/projects/models.py:69 +msgid "invitation extra text" +msgstr "" + +#: taiga/projects/models.py:75 +msgid "The user is already member of the project" +msgstr "" + +#: taiga/projects/models.py:90 +msgid "default points" +msgstr "" + +#: taiga/projects/models.py:94 +msgid "default US status" +msgstr "" + +#: taiga/projects/models.py:98 +msgid "default task status" +msgstr "" + +#: taiga/projects/models.py:101 +msgid "default priority" +msgstr "" + +#: taiga/projects/models.py:104 +msgid "default severity" +msgstr "" + +#: taiga/projects/models.py:108 +msgid "default issue status" +msgstr "" + +#: taiga/projects/models.py:112 +msgid "default issue type" +msgstr "" + +#: taiga/projects/models.py:133 +msgid "members" +msgstr "" + +#: taiga/projects/models.py:136 +msgid "total of milestones" +msgstr "" + +#: taiga/projects/models.py:137 +msgid "total story points" +msgstr "" + +#: taiga/projects/models.py:140 taiga/projects/models.py:558 +msgid "active backlog panel" +msgstr "" + +#: taiga/projects/models.py:142 taiga/projects/models.py:560 +msgid "active kanban panel" +msgstr "" + +#: taiga/projects/models.py:144 taiga/projects/models.py:562 +msgid "active wiki panel" +msgstr "" + +#: taiga/projects/models.py:146 taiga/projects/models.py:564 +msgid "active issues panel" +msgstr "" + +#: taiga/projects/models.py:149 taiga/projects/models.py:567 +msgid "videoconference system" +msgstr "" + +#: taiga/projects/models.py:151 taiga/projects/models.py:569 +msgid "videoconference room salt" +msgstr "" + +#: taiga/projects/models.py:156 +msgid "creation template" +msgstr "" + +#: taiga/projects/models.py:159 +msgid "anonymous permissions" +msgstr "" + +#: taiga/projects/models.py:163 +msgid "user permissions" +msgstr "" + +#: taiga/projects/models.py:166 +msgid "is private" +msgstr "" + +#: taiga/projects/models.py:177 +msgid "tags colors" +msgstr "" + +#: taiga/projects/models.py:327 +msgid "modules config" +msgstr "" + +#: taiga/projects/models.py:346 +msgid "is archived" +msgstr "" + +#: taiga/projects/models.py:348 taiga/projects/models.py:410 +#: taiga/projects/models.py:443 taiga/projects/models.py:466 +#: taiga/projects/models.py:493 taiga/projects/models.py:524 +#: taiga/users/models.py:113 +msgid "color" +msgstr "" + +#: taiga/projects/models.py:350 +msgid "work in progress limit" +msgstr "" + +#: taiga/projects/models.py:381 taiga/userstorage/models.py:31 +msgid "value" +msgstr "" + +#: taiga/projects/models.py:555 +msgid "default owner's role" +msgstr "" + +#: taiga/projects/models.py:571 +msgid "default options" +msgstr "" + +#: taiga/projects/models.py:572 +msgid "us statuses" +msgstr "" + +#: taiga/projects/models.py:573 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 +msgid "points" +msgstr "" + +#: taiga/projects/models.py:574 +msgid "task statuses" +msgstr "" + +#: taiga/projects/models.py:575 +msgid "issue statuses" +msgstr "" + +#: taiga/projects/models.py:576 +msgid "issue types" +msgstr "" + +#: taiga/projects/models.py:577 +msgid "priorities" +msgstr "" + +#: taiga/projects/models.py:578 +msgid "severities" +msgstr "" + +#: taiga/projects/models.py:579 +msgid "roles" +msgstr "" + +#: taiga/projects/notifications/choices.py:28 +msgid "Not watching" +msgstr "" + +#: taiga/projects/notifications/choices.py:29 +msgid "Watching" +msgstr "" + +#: taiga/projects/notifications/choices.py:30 +msgid "Ignoring" +msgstr "" + +#: taiga/projects/notifications/mixins.py:87 +msgid "watchers" +msgstr "" + +#: taiga/projects/notifications/models.py:57 +msgid "created date time" +msgstr "" + +#: taiga/projects/notifications/models.py:59 +msgid "updated date time" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" + +#: taiga/projects/occ/mixins.py:91 +msgid "version" +msgstr "" + +#: taiga/projects/permissions.py:39 +msgid "You can't leave the project if there are no more owners" +msgstr "" + +#: taiga/projects/serializers.py:203 +msgid "Email address is already taken" +msgstr "" + +#: taiga/projects/serializers.py:215 +msgid "Invalid role for the project" +msgstr "" + +#: taiga/projects/serializers.py:370 +msgid "Default options" +msgstr "" + +#: taiga/projects/serializers.py:371 +msgid "User story's statuses" +msgstr "" + +#: taiga/projects/serializers.py:372 +msgid "Points" +msgstr "" + +#: taiga/projects/serializers.py:373 +msgid "Task's statuses" +msgstr "" + +#: taiga/projects/serializers.py:374 +msgid "Issue's statuses" +msgstr "" + +#: taiga/projects/serializers.py:375 +msgid "Issue's types" +msgstr "" + +#: taiga/projects/serializers.py:376 +msgid "Priorities" +msgstr "" + +#: taiga/projects/serializers.py:377 +msgid "Severities" +msgstr "" + +#: taiga/projects/serializers.py:378 +msgid "Roles" +msgstr "" + +#: taiga/projects/tasks/api.py:57 taiga/projects/tasks/api.py:60 +#: taiga/projects/tasks/api.py:63 taiga/projects/tasks/api.py:66 +msgid "You don't have permissions for add/modify this task." +msgstr "" + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:18 +#, python-format +msgid "" +"\n" +"

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "Accept your invitation to Taiga" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "Accept your invitation" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:26 +msgid "The Taiga Team" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:19 +msgid "Accept your invitation to Taiga following whis link:" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:21 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" + +#: taiga/projects/userstories/api.py:170 +#, python-brace-format +msgid "" +"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " +"{subject}\")" +msgstr "" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "" + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "" + +#: taiga/projects/userstories/validators.py:12 +msgid "There's no user story with that id" +msgstr "" + +#: taiga/projects/validators.py:12 +msgid "There's no project with that id" +msgstr "" + +#: taiga/projects/validators.py:21 +msgid "There's no user story status with that id" +msgstr "" + +#: taiga/projects/validators.py:30 +msgid "There's no task status with that id" +msgstr "" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:54 +msgid "Votes" +msgstr "" + +#: taiga/projects/votes/models.py:50 +msgid "votes" +msgstr "" + +#: taiga/projects/votes/models.py:53 +msgid "Vote" +msgstr "" + +#: taiga/projects/wiki/api.py:60 +msgid "No content parameter" +msgstr "" + +#: taiga/projects/wiki/api.py:63 +msgid "No project_id parameter" +msgstr "" + +#: taiga/projects/wiki/models.py:36 +msgid "last modifier" +msgstr "" + +#: taiga/projects/wiki/models.py:69 +msgid "href" +msgstr "" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "" + +#: taiga/users/api.py:87 taiga/users/api.py:94 +msgid "Invalid username or email" +msgstr "" + +#: taiga/users/api.py:103 +msgid "Mail sended successful!" +msgstr "" + +#: taiga/users/api.py:116 taiga/users/api.py:121 +msgid "Token is invalid" +msgstr "" + +#: taiga/users/api.py:142 +msgid "Current password parameter needed" +msgstr "" + +#: taiga/users/api.py:145 +msgid "New password parameter needed" +msgstr "" + +#: taiga/users/api.py:148 +msgid "Invalid password length at least 6 charaters needed" +msgstr "" + +#: taiga/users/api.py:151 +msgid "Invalid current password" +msgstr "" + +#: taiga/users/api.py:167 +msgid "Incomplete arguments" +msgstr "" + +#: taiga/users/api.py:172 +msgid "Invalid image format" +msgstr "" + +#: taiga/users/api.py:225 +msgid "Duplicated email" +msgstr "" + +#: taiga/users/api.py:227 +msgid "Not valid email" +msgstr "" + +#: taiga/users/api.py:246 taiga/users/api.py:252 +msgid "" +"Invalid, are you sure the token is correct and you didn't use it before?" +msgstr "" + +#: taiga/users/api.py:279 taiga/users/api.py:287 taiga/users/api.py:290 +msgid "Invalid, are you sure the token is correct?" +msgstr "" + +#: taiga/users/models.py:69 +msgid "superuser status" +msgstr "" + +#: taiga/users/models.py:70 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" + +#: taiga/users/models.py:100 +msgid "username" +msgstr "" + +#: taiga/users/models.py:101 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" +msgstr "" + +#: taiga/users/models.py:104 +msgid "Enter a valid username." +msgstr "" + +#: taiga/users/models.py:107 +msgid "active" +msgstr "" + +#: taiga/users/models.py:108 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" + +#: taiga/users/models.py:114 +msgid "biography" +msgstr "" + +#: taiga/users/models.py:117 +msgid "photo" +msgstr "" + +#: taiga/users/models.py:118 +msgid "date joined" +msgstr "" + +#: taiga/users/models.py:120 +msgid "default language" +msgstr "" + +#: taiga/users/models.py:122 +msgid "default timezone" +msgstr "" + +#: taiga/users/models.py:124 +msgid "colorize tags" +msgstr "" + +#: taiga/users/models.py:129 +msgid "email token" +msgstr "" + +#: taiga/users/models.py:131 +msgid "new email address" +msgstr "" + +#: taiga/users/models.py:185 +msgid "permissions" +msgstr "" + +#: taiga/users/serializers.py:52 +msgid "invalid username" +msgstr "" + +#: taiga/users/serializers.py:53 +msgid "invalid" +msgstr "" + +#: taiga/users/serializers.py:58 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "" + +#: taiga/users/serializers.py:64 +msgid "Invalid username. Try with a different one." +msgstr "" + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "" + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "" + +#: taiga/webhooks/models.py:39 +msgid "Status code" +msgstr "" + +#: taiga/webhooks/models.py:40 +msgid "Request data" +msgstr "" + +#: taiga/webhooks/models.py:41 +msgid "Request headers" +msgstr "" + +#: taiga/webhooks/models.py:42 +msgid "Response data" +msgstr "" + +#: taiga/webhooks/models.py:43 +msgid "Response headers" +msgstr "" + +#: taiga/webhooks/models.py:44 +msgid "Duration" +msgstr "" diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index e9e3458a..e0a37ae6 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -1,594 +1,1535 @@ -# SPANISH LANGUAGE PACKAGE FOR TAIGA IO. -# Copyright (C) 2014 -# This file is distributed under the same license as the taiga io package. -# FIRST AUTHOR , YEAR. +# taiga-back.taiga. +# Copyright (C) 2015 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. # -#. Spanish Translators: please, consider don't use informal expressions -#. Traductores al español, por favor, es necesario considerar el uso -#. de la 3era persona en lugar del "tú" -#, fuzzy +# Translators: +# Esther Moreno , 2015 +# Taiga Dev Team , 2015 msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-12-09 22:06+0100\n" -"PO-Revision-Date: 2014-12-19 19:48-0430\n" -"Last-Translator: Hector Colina \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"PO-Revision-Date: 2015-03-26 09:53+0000\n" +"Last-Translator: Esther Moreno \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/taiga-back/" +"language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: base/exceptions.py:16 +#: taiga/auth/api.py:100 +msgid "Public register is disabled." +msgstr "El registro público está deshabilitado." + +#: taiga/auth/api.py:133 +msgid "invalid register type" +msgstr "Tipo de registro inválido" + +#: taiga/auth/api.py:146 +msgid "invalid login type" +msgstr "Tipo de login inválido" + +#: taiga/auth/services.py:75 +msgid "Username is already in use." +msgstr "Nombre de usuario no disponible" + +#: taiga/auth/services.py:78 +msgid "Email is already in use." +msgstr "Email no disponible" + +#: taiga/auth/services.py:172 +msgid "Error on creating new user." +msgstr "Error al crear un nuevo usuario " + +#: taiga/base/api/generics.py:162 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "" + +#: taiga/base/api/generics.py:166 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Página no válida (%(page_number)s): %(message)s" + +#: taiga/base/connectors/exceptions.py:24 +msgid "Connection error." +msgstr "Error de conexión" + +#: taiga/base/exceptions.py:30 msgid "Unexpected error" msgstr "Error inesperado" -#: base/exceptions.py:28 +#: taiga/base/exceptions.py:42 msgid "Not found." -msgstr "No encontrado." +msgstr "" -#: base/exceptions.py:36 base/exceptions.py:44 +#: taiga/base/exceptions.py:47 +msgid "Method not supported for this endpoint." +msgstr "" + +#: taiga/base/exceptions.py:55 taiga/base/exceptions.py:63 msgid "Wrong arguments." -msgstr "Argumentos erróneos." +msgstr "" -#: base/exceptions.py:59 +#: taiga/base/exceptions.py:67 +msgid "Data validation error" +msgstr "" + +#: taiga/base/exceptions.py:79 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "" + +#: taiga/base/exceptions.py:86 msgid "Precondition error" -msgstr "Error en precondición" +msgstr "" -#: base/exceptions.py:67 -msgid "Internal server error" -msgstr "Error interno del servidor" - -#: base/exceptions.py:117 -msgid "Not found" -msgstr "No encontrado" - -#: base/exceptions.py:121 -msgid "Permission denied" -msgstr "Permiso denegado" - -#: base/auth/api.py:52 -msgid "Public register is disabled for this domain." -msgstr "El registro público está deshabilitado para este dominio." - -#: base/auth/api.py:91 -msgid "Invalid token" -msgstr "Token inválido" - -#: base/auth/api.py:100 -msgid "Incorrect password" -msgstr "Contraseña incorrecta" - -#: base/auth/api.py:133 -msgid "invalid register type" -msgstr "tipo de registro no válido" - -#: base/auth/api.py:142 base/auth/api.py:145 -msgid "Invalid username or password" -msgstr "usuario o contraseña inválidos" - -#: base/domains/__init__.py:54 -msgid "domain not found" -msgstr "dominio no encontrado" - -#: base/domains/models.py:24 -msgid "The domain name cannot contain any spaces or tabs." -msgstr "El nombre de dominio no puede tener espacios o tabuladores." - -#: base/domains/models.py:30 -msgid "domain name" -msgstr "nombre del dominio" - -#: base/domains/models.py:32 -msgid "display name" -msgstr "nombre mostrado" - -#: base/domains/models.py:33 -msgid "scheme" -msgstr "esquema" - -#: base/domains/models.py:38 base/users/models.py:19 -msgid "default language" -msgstr "idioma por defecto" - -#: base/domains/models.py:41 base/domains/models.py:42 -msgid "domain" -msgstr "dominio" - -#: base/notifications/models.py:12 -msgid "All events on my projects" -msgstr "Todos los eventos en mis proyectos" - -#: base/notifications/models.py:13 -msgid "Only events for objects i watch" -msgstr "Sólo eventos para objetos a los cuales les hago seguimiento" - -#: base/notifications/models.py:14 -msgid "Only events for objects assigned to me" -msgstr "Sólo eventos para objetos que me han sido asignados" - -#: base/notifications/models.py:15 -msgid "Only events for objects owned by me" -msgstr "Sólo eventos para mis objetos" - -#: base/notifications/models.py:16 -msgid "No events" -msgstr "Sin eventos" - -#: base/notifications/models.py:22 -msgid "notify level" -msgstr "nivel de notificación" - -#: base/notifications/models.py:24 -msgid "notify changes by me" -msgstr "notificar mis cambios" - -#: base/users/admin.py:36 -msgid "Personal info" -msgstr "Información personal" - -#: base/users/admin.py:37 -msgid "Extra info" -msgstr "Información extra" - -#: base/users/admin.py:38 -msgid "Notifications info" -msgstr "Información de notificaciones" - -#: base/users/admin.py:39 -msgid "Permissions" -msgstr "Permisos" - -#: base/users/admin.py:40 -msgid "Important dates" -msgstr "fechas importantes" - -#: base/users/api.py:45 base/users/api.py:52 -msgid "Invalid username or email" -msgstr "usuario o correos inválidos" - -#: base/users/api.py:61 -msgid "Mail sended successful!" -msgstr "¡Correo enviado correctamente!" - -#: base/users/api.py:70 -msgid "Token is invalid" -msgstr "El token no es válido" - -#: base/users/api.py:87 -msgid "Incomplete arguments" -msgstr "Argumentos incompletos" - -#: base/users/api.py:90 -msgid "Invalid password length" -msgstr "longitud de la contraseña no válida" - -#: base/users/models.py:13 projects/models.py:281 projects/models.py:331 -#: projects/models.py:356 projects/models.py:379 projects/models.py:404 -#: projects/models.py:427 projects/models.py:454 -msgid "color" -msgstr "color" - -#: base/users/models.py:15 projects/models.py:95 -#: projects/documents/models.py:17 projects/issues/models.py:44 -#: projects/tasks/models.py:40 projects/userstories/models.py:68 -msgid "description" -msgstr "descripción" - -#: base/users/models.py:17 -msgid "photo" -msgstr "fotografía" - -#: base/users/models.py:21 -msgid "default timezone" -msgstr "zona horaria por defecto" - -#: base/users/models.py:23 -msgid "token" -msgstr "token" - -#: base/users/models.py:25 -msgid "colorize tags" -msgstr "etiquetas coloreadas" - -#: base/users/models.py:45 projects/models.py:91 projects/models.py:275 -#: projects/models.py:300 projects/models.py:325 projects/models.py:352 -#: projects/models.py:375 projects/models.py:398 projects/models.py:423 -#: projects/models.py:448 projects/milestones/models.py:20 -msgid "name" -msgstr "nombre" - -#: base/users/models.py:47 projects/models.py:93 -#: projects/documents/models.py:13 projects/milestones/models.py:22 -#: projects/wiki/models.py:15 -msgid "slug" -msgstr "slug" - -#: base/users/models.py:49 -msgid "permissions" -msgstr "permisos" - -#: base/users/models.py:51 projects/models.py:277 projects/models.py:302 -#: projects/models.py:327 projects/models.py:354 projects/models.py:377 -#: projects/models.py:400 projects/models.py:425 projects/models.py:450 -#: projects/milestones/models.py:40 projects/userstories/models.py:59 -msgid "order" -msgstr "orden" - -#: base/users/serializers.py:33 -msgid "invalid token" -msgstr "token inválido" - -#: projects/api.py:89 -msgid "Email address is already taken." -msgstr "Dirección de correo ya utilizada" - -#: projects/choices.py:7 -msgid "Open" -msgstr "Abierta" - -#: projects/choices.py:8 projects/choices.py:15 projects/choices.py:52 -#: projects/choices.py:65 -msgid "Closed" -msgstr "Cerrada" - -#: projects/choices.py:12 projects/choices.py:49 -msgid "New" -msgstr "Nueva" - -#: projects/choices.py:13 projects/choices.py:50 -msgid "In progress" -msgstr "En progreso" - -#: projects/choices.py:14 projects/choices.py:51 -msgid "Ready for test" -msgstr "Listo para probar" - -#: projects/choices.py:16 projects/choices.py:53 -msgid "Needs Info" -msgstr "Necesita información" - -#: projects/choices.py:35 -msgid "Low" -msgstr "Baja" - -#: projects/choices.py:36 projects/choices.py:43 -msgid "Normal" -msgstr "Normal" - -#: projects/choices.py:37 -msgid "High" -msgstr "Alta" - -#: projects/choices.py:41 -msgid "Wishlist" -msgstr "Deseo" - -#: projects/choices.py:42 -msgid "Minor" -msgstr "Menor" - -#: projects/choices.py:44 -msgid "Important" -msgstr "Importante" - -#: projects/choices.py:45 -msgid "Critical" -msgstr "Crítica" - -#: projects/choices.py:54 -msgid "Rejected" -msgstr "Rechazada" - -#: projects/choices.py:55 -msgid "Postponed" -msgstr "Pospuesto" - -#: projects/choices.py:59 -msgid "Bug" -msgstr "Bug" - -#: projects/choices.py:63 -msgid "Pending" -msgstr "Pendiente" - -#: projects/choices.py:64 -msgid "Answered" -msgstr "Respondida" - -#: projects/models.py:57 -msgid "default points" -msgstr "puntos por defecto" - -#: projects/models.py:61 -msgid "default US status" -msgstr "estado por defecto de las USs" - -#: projects/models.py:65 -msgid "default task status" -msgstr "estado por defecto de las tareas" - -#: projects/models.py:68 -msgid "default priority" -msgstr "prioridad por defecto" - -#: projects/models.py:71 -msgid "default severity" -msgstr "severidad por defecto" - -#: projects/models.py:75 -msgid "default issue status" -msgstr "estado por defecto de las peticiones" - -#: projects/models.py:79 -msgid "default issue type" -msgstr "tipo por defecto de las peticiones" - -#: projects/models.py:83 -msgid "default questions status" -msgstr "Estado por defecto de las preguntas" - -#: projects/models.py:97 projects/models.py:253 -#: projects/documents/models.py:19 projects/issues/models.py:37 -#: projects/milestones/models.py:32 projects/questions/models.py:39 -#: projects/tasks/models.py:33 projects/userstories/models.py:61 -#: projects/wiki/models.py:24 -msgid "created date" -msgstr "fecha de creación" - -#: projects/models.py:99 projects/models.py:255 -#: projects/documents/models.py:21 projects/issues/models.py:39 -#: projects/milestones/models.py:34 projects/questions/models.py:41 -#: projects/tasks/models.py:35 projects/userstories/models.py:63 -#: projects/wiki/models.py:26 -msgid "modified date" -msgstr "fecha de modificación" - -#: projects/models.py:101 projects/models.py:244 -#: projects/documents/models.py:27 projects/issues/models.py:22 -#: projects/milestones/models.py:24 projects/questions/models.py:22 -#: projects/tasks/models.py:24 projects/userstories/models.py:49 -#: projects/wiki/models.py:19 -msgid "owner" -msgstr "propietario" - -#: projects/models.py:103 -msgid "members" -msgstr "miembros" - -#: projects/models.py:105 -msgid "public" -msgstr "público" - -#: projects/models.py:107 -msgid "last us ref" -msgstr "última referencia de US" - -#: projects/models.py:109 -msgid "last task ref" -msgstr "última referencia de tarea" - -#: projects/models.py:111 -msgid "last issue ref" -msgstr "última referencia de issue" - -#: projects/models.py:113 -msgid "total of milestones" -msgstr "total de sprints" - -#: projects/models.py:115 -msgid "total story points" -msgstr "total de puntos de historia" - -#: projects/models.py:116 projects/documents/models.py:32 -#: projects/issues/models.py:51 projects/questions/models.py:45 -#: projects/tasks/models.py:46 projects/userstories/models.py:76 +#: taiga/base/tags.py:25 msgid "tags" msgstr "etiquetas" -#: projects/models.py:246 projects/models.py:283 projects/models.py:306 -#: projects/models.py:333 projects/models.py:358 projects/models.py:381 -#: projects/models.py:406 projects/models.py:429 projects/models.py:457 -#: projects/documents/models.py:24 projects/issues/models.py:35 -#: projects/milestones/models.py:26 projects/questions/models.py:29 -#: projects/tasks/models.py:28 projects/userstories/models.py:47 -#: projects/wiki/models.py:13 +#: taiga/base/templates/emails/base-body-html.jinja:6 +msgid "Taiga" +msgstr "Taiga" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Follow us on Twitter" +msgstr "Síguenos en Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:406 +#: taiga/base/templates/emails/hero-body-html.jinja:380 +#: taiga/base/templates/emails/updates-body-html.jinja:442 +msgid "Twitter" +msgstr "Twitter" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "Get the code on GitHub" +msgstr "Copia el código en GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:407 +#: taiga/base/templates/emails/hero-body-html.jinja:381 +#: taiga/base/templates/emails/updates-body-html.jinja:443 +msgid "GitHub" +msgstr "GitHub" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Visit our website" +msgstr "Visita nuestra web" + +#: taiga/base/templates/emails/base-body-html.jinja:408 +#: taiga/base/templates/emails/hero-body-html.jinja:382 +#: taiga/base/templates/emails/updates-body-html.jinja:444 +msgid "Taiga.io" +msgstr "Taiga.io" + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Te hemos Taigatizado" + +#: taiga/base/templates/emails/hero-body-html.jinja:359 +msgid "" +"\n" +"

You have been Taigatized!" +"

\n" +"

Welcome to Taiga, an Open " +"Source, Agile Project Management Tool

\n" +" " +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:6 +msgid "[Taiga] Updates" +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

\n" +" " +msgstr "" + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" + +#: taiga/export_import/api.py:183 +msgid "Needed dump file" +msgstr "" + +#: taiga/export_import/api.py:190 +msgid "Invalid dump format" +msgstr "" + +#: taiga/export_import/serializers.py:377 +#: taiga/projects/custom_attributes/serializers.py:104 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "" + +#: taiga/export_import/serializers.py:392 +#: taiga/projects/custom_attributes/serializers.py:119 +msgid "It contain invalid custom fields." +msgstr "" + +#: taiga/export_import/templates/emails/dump_project-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been generated" +msgstr "" + +#: taiga/export_import/templates/emails/export_error-subject.jinja:1 +#, python-format +msgid "[%(project)s] %(error_subject)s" +msgstr "" + +#: taiga/export_import/templates/emails/import_error-subject.jinja:1 +#, python-format +msgid "[Taiga] %(error_subject)s" +msgstr "" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "" + +#: taiga/feedback/models.py:23 taiga/users/models.py:111 +msgid "full name" +msgstr "nombre completo" + +#: taiga/feedback/models.py:25 taiga/users/models.py:106 +msgid "email address" +msgstr "dirección de email" + +#: taiga/feedback/models.py:27 +msgid "comment" +msgstr "" + +#: taiga/feedback/models.py:29 taiga/projects/attachments/models.py:63 +#: taiga/projects/custom_attributes/models.py:38 +#: taiga/projects/issues/models.py:53 taiga/projects/milestones/models.py:45 +#: taiga/projects/models.py:126 taiga/projects/models.py:549 +#: taiga/projects/tasks/models.py:46 taiga/projects/userstories/models.py:82 +#: taiga/projects/wiki/models.py:38 taiga/userstorage/models.py:27 +msgid "created date" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Feedback

\n" +"

Taiga has received feedback from %(full_name)s <%(email)s>

\n" +" " +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:9 +#, python-format +msgid "" +"\n" +"

Comment

\n" +"

%(comment)s

\n" +" " +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/users/admin.py:51 +msgid "Extra info" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:1 +#, python-format +msgid "" +"---------\n" +"- From: %(full_name)s <%(email)s>\n" +"---------\n" +"- Comment:\n" +"%(comment)s\n" +"---------" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "" + +#: taiga/feedback/templates/emails/feedback_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Feedback from %(full_name)s <%(email)s>\n" +msgstr "" + +#: taiga/hooks/api.py:52 +msgid "The payload is not a valid json" +msgstr "" + +#: taiga/hooks/api.py:61 +msgid "The project doesn't exist" +msgstr "El proyecto no existe" + +#: taiga/hooks/api.py:64 +msgid "Bad signature" +msgstr "" + +#: taiga/hooks/bitbucket/api.py:40 +msgid "The payload is not a valid application/x-www-form-urlencoded" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:46 +msgid "The payload is not valid" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:82 +#: taiga/hooks/github/event_hooks.py:75 taiga/hooks/gitlab/event_hooks.py:74 +msgid "The referenced element doesn't exist" +msgstr "" + +#: taiga/hooks/bitbucket/event_hooks.py:89 +#: taiga/hooks/github/event_hooks.py:82 taiga/hooks/gitlab/event_hooks.py:81 +msgid "The status doesn't exist" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:113 taiga/hooks/gitlab/event_hooks.py:114 +msgid "Invalid issue information" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:135 taiga/hooks/github/event_hooks.py:144 +msgid "Invalid issue comment information" +msgstr "" + +#: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 +#: taiga/permissions/permissions.py:52 +msgid "View project" +msgstr "" + +#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 +#: taiga/permissions/permissions.py:54 +msgid "View milestones" +msgstr "" + +#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 +msgid "View user stories" +msgstr "Ver historias de usuarios" + +#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:36 +#: taiga/permissions/permissions.py:64 +msgid "View tasks" +msgstr "Ver tareas" + +#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:34 +#: taiga/permissions/permissions.py:69 +msgid "View issues" +msgstr "" + +#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:37 +#: taiga/permissions/permissions.py:75 +msgid "View wiki pages" +msgstr "Ver páginas del wiki" + +#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:38 +#: taiga/permissions/permissions.py:80 +msgid "View wiki links" +msgstr "Ver enlaces del wiki" + +#: taiga/permissions/permissions.py:35 taiga/permissions/permissions.py:70 +msgid "Vote issues" +msgstr "" + +#: taiga/permissions/permissions.py:39 +msgid "Request membership" +msgstr "" + +#: taiga/permissions/permissions.py:40 +msgid "Add user story to project" +msgstr "Añadir historias de usuario al proyecto" + +#: taiga/permissions/permissions.py:41 +msgid "Add comments to user stories" +msgstr "" + +#: taiga/permissions/permissions.py:42 +msgid "Add comments to tasks" +msgstr "" + +#: taiga/permissions/permissions.py:43 +msgid "Add issues" +msgstr "" + +#: taiga/permissions/permissions.py:44 +msgid "Add comments to issues" +msgstr "" + +#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:76 +msgid "Add wiki page" +msgstr "" + +#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:77 +msgid "Modify wiki page" +msgstr "" + +#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:81 +msgid "Add wiki link" +msgstr "" + +#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:82 +msgid "Modify wiki link" +msgstr "" + +#: taiga/permissions/permissions.py:55 +msgid "Add milestone" +msgstr "" + +#: taiga/permissions/permissions.py:56 +msgid "Modify milestone" +msgstr "" + +#: taiga/permissions/permissions.py:57 +msgid "Delete milestone" +msgstr "" + +#: taiga/permissions/permissions.py:59 +msgid "View user story" +msgstr "" + +#: taiga/permissions/permissions.py:60 +msgid "Add user story" +msgstr "" + +#: taiga/permissions/permissions.py:61 +msgid "Modify user story" +msgstr "" + +#: taiga/permissions/permissions.py:62 +msgid "Delete user story" +msgstr "" + +#: taiga/permissions/permissions.py:65 +msgid "Add task" +msgstr "" + +#: taiga/permissions/permissions.py:66 +msgid "Modify task" +msgstr "" + +#: taiga/permissions/permissions.py:67 +msgid "Delete task" +msgstr "" + +#: taiga/permissions/permissions.py:71 +msgid "Add issue" +msgstr "" + +#: taiga/permissions/permissions.py:72 +msgid "Modify issue" +msgstr "" + +#: taiga/permissions/permissions.py:73 +msgid "Delete issue" +msgstr "" + +#: taiga/permissions/permissions.py:78 +msgid "Delete wiki page" +msgstr "" + +#: taiga/permissions/permissions.py:83 +msgid "Delete wiki link" +msgstr "" + +#: taiga/permissions/permissions.py:87 +msgid "Modify project" +msgstr "" + +#: taiga/permissions/permissions.py:88 +msgid "Add member" +msgstr "" + +#: taiga/permissions/permissions.py:89 +msgid "Remove member" +msgstr "" + +#: taiga/permissions/permissions.py:90 +msgid "Delete project" +msgstr "" + +#: taiga/permissions/permissions.py:91 +msgid "Admin project values" +msgstr "" + +#: taiga/permissions/permissions.py:92 +msgid "Admin roles" +msgstr "" + +#: taiga/projects/api.py:454 taiga/projects/serializers.py:227 +msgid "At least one of the user must be an active admin" +msgstr "" + +#: taiga/projects/api.py:484 +msgid "You don't have permisions to see that." +msgstr "" + +#: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 +#: taiga/projects/milestones/models.py:39 taiga/projects/models.py:131 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:64 +#: taiga/projects/wiki/models.py:34 taiga/userstorage/models.py:25 +msgid "owner" +msgstr "" + +#: taiga/projects/attachments/models.py:56 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/issues/models.py:51 taiga/projects/milestones/models.py:41 +#: taiga/projects/models.py:326 taiga/projects/models.py:352 +#: taiga/projects/models.py:383 taiga/projects/models.py:412 +#: taiga/projects/models.py:445 taiga/projects/models.py:468 +#: taiga/projects/models.py:495 taiga/projects/models.py:526 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:62 +#: taiga/projects/wiki/models.py:28 taiga/projects/wiki/models.py:66 +#: taiga/users/models.py:193 msgid "project" -msgstr "proyecto" +msgstr "" -#: projects/models.py:248 +#: taiga/projects/attachments/models.py:58 msgid "content type" -msgstr "tipo de contenido" +msgstr "" -#: projects/models.py:250 +#: taiga/projects/attachments/models.py:60 msgid "object id" -msgstr "id de objeto" +msgstr "" -#: projects/models.py:258 +#: taiga/projects/attachments/models.py:66 +#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/issues/models.py:56 taiga/projects/milestones/models.py:48 +#: taiga/projects/models.py:129 taiga/projects/models.py:552 +#: taiga/projects/tasks/models.py:49 taiga/projects/userstories/models.py:85 +#: taiga/projects/wiki/models.py:41 taiga/userstorage/models.py:29 +msgid "modified date" +msgstr "" + +#: taiga/projects/attachments/models.py:71 msgid "attached file" -msgstr "fichero adjunto" +msgstr "" -#: projects/models.py:279 projects/models.py:329 projects/models.py:402 -#: projects/models.py:452 projects/milestones/models.py:36 -msgid "is closed" -msgstr "está cerrado" +#: taiga/projects/attachments/models.py:74 +msgid "is deprecated" +msgstr "" -#: projects/models.py:304 -msgid "value" -msgstr "valor" +#: taiga/projects/attachments/models.py:75 +#: taiga/projects/custom_attributes/models.py:32 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:61 taiga/projects/models.py:124 +#: taiga/projects/models.py:547 taiga/projects/tasks/models.py:60 +#: taiga/projects/userstories/models.py:90 +msgid "description" +msgstr "" -#: projects/documents/models.py:15 -msgid "title" -msgstr "título" +#: taiga/projects/attachments/models.py:76 +#: taiga/projects/custom_attributes/models.py:33 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:342 +#: taiga/projects/models.py:379 taiga/projects/models.py:406 +#: taiga/projects/models.py:441 taiga/projects/models.py:464 +#: taiga/projects/models.py:489 taiga/projects/models.py:522 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:188 +msgid "order" +msgstr "" -#: projects/documents/models.py:30 -msgid "attached_file" -msgstr "fichero_adjunto" +#: taiga/projects/custom_attributes/models.py:31 +#: taiga/projects/milestones/models.py:34 taiga/projects/models.py:120 +#: taiga/projects/models.py:338 taiga/projects/models.py:377 +#: taiga/projects/models.py:402 taiga/projects/models.py:439 +#: taiga/projects/models.py:462 taiga/projects/models.py:485 +#: taiga/projects/models.py:520 taiga/projects/models.py:543 +#: taiga/users/models.py:180 taiga/webhooks/models.py:27 +msgid "name" +msgstr "" -#: projects/issues/api.py:82 projects/issues/api.py:85 -#: projects/issues/api.py:88 projects/issues/api.py:91 -#: projects/issues/api.py:94 projects/issues/api.py:97 -msgid "You don't have permissions for add/modify this issue." -msgstr "No tienes permisos para crear/modificar esta petición." +#: taiga/projects/custom_attributes/models.py:81 +msgid "attributes_values" +msgstr "" -#: projects/issues/api.py:131 -msgid "You don't have permissions for add attachments to this issue" -msgstr "No tienes permisos para añadir ficheros adjuntos a esta petición" - -#: projects/issues/models.py:20 projects/questions/models.py:20 -#: projects/tasks/models.py:22 projects/userstories/models.py:42 -msgid "ref" -msgstr "ref" - -#: projects/issues/models.py:24 projects/questions/models.py:24 -#: projects/tasks/models.py:26 projects/userstories/models.py:52 -msgid "status" -msgstr "estado" - -#: projects/issues/models.py:26 -msgid "severity" -msgstr "severidad" - -#: projects/issues/models.py:28 -msgid "priority" -msgstr "prioridad" - -#: projects/issues/models.py:30 -msgid "type" -msgstr "tipo" - -#: projects/issues/models.py:33 projects/questions/models.py:32 -#: projects/tasks/models.py:31 projects/userstories/models.py:45 -msgid "milestone" -msgstr "sprint" - -#: projects/issues/models.py:41 projects/questions/models.py:34 -#: projects/tasks/models.py:37 -msgid "finished date" -msgstr "fecha de fin" - -#: projects/issues/models.py:43 projects/questions/models.py:26 -#: projects/tasks/models.py:39 projects/userstories/models.py:67 -msgid "subject" -msgstr "asunto" - -#: projects/issues/models.py:47 projects/tasks/models.py:43 -msgid "assigned to" -msgstr "assignada a" - -#: projects/issues/models.py:50 projects/questions/models.py:44 -#: projects/tasks/models.py:45 projects/userstories/models.py:70 -#: projects/wiki/models.py:22 -msgid "watchers" -msgstr "observadores" - -#: projects/issues/models.py:86 projects/tasks/models.py:76 -msgid "Unassigned" -msgstr "Sin asignar" - -#: projects/milestones/api.py:37 -msgid "You must not add a new milestone to this project." -msgstr "No debes añadir un nuevo sprint a este proyecto." - -#: projects/milestones/models.py:28 -msgid "estimated start" -msgstr "inicio estimado" - -#: projects/milestones/models.py:30 -msgid "estimated finish" -msgstr "fin estimado" - -#: projects/milestones/models.py:38 -msgid "disponibility" -msgstr "disponibilidad" - -#: projects/questions/models.py:27 projects/wiki/models.py:17 -msgid "content" -msgstr "contenido" - -#: projects/questions/models.py:37 -msgid "assigned_to" -msgstr "assignada_a" - -#: projects/tasks/api.py:47 -msgid "You don't have permissions for add attachments to this task." -msgstr "No tienes permisos para añadir ficheros adjuntos a esta tarea." - -#: projects/tasks/api.py:74 projects/tasks/api.py:77 projects/tasks/api.py:80 -#: projects/tasks/api.py:83 -msgid "You don't have permissions for add/modify this task." -msgstr "No tienes permisos para añadir/modificar esta tarea." - -#: projects/tasks/models.py:20 projects/userstories/models.py:20 +#: taiga/projects/custom_attributes/models.py:91 +#: taiga/projects/tasks/models.py:33 taiga/projects/userstories/models.py:34 msgid "user story" -msgstr "historia de usuario" - -#: projects/tasks/models.py:49 -msgid "is iocaine" -msgstr "es iocaina" - -#: projects/userstories/api.py:55 -msgid "You don't have permissions for add attachments to this user story" msgstr "" -"No tienes permisos para añadir ficheros adjuntos a esta historia de " -"usuario." -#: projects/userstories/api.py:75 projects/userstories/api.py:99 -msgid "bulkStories parameter is mandatory" -msgstr "El parámetro bulkStories es obligatorio" +#: taiga/projects/custom_attributes/models.py:106 +msgid "task" +msgstr "" -#: projects/userstories/api.py:79 -msgid "projectId parameter is mandatory" -msgstr "El parámetro projectID es obligatorio" +#: taiga/projects/custom_attributes/models.py:121 +msgid "issue" +msgstr "" -#: projects/userstories/api.py:84 projects/userstories/api.py:108 -msgid "You don't have permisions to create user stories." -msgstr "No tienes permisos para crear historias de usuario." +#: taiga/projects/custom_attributes/serializers.py:58 +msgid "Already exists one with the same name." +msgstr "" -#: projects/userstories/api.py:103 -msgid "projectId parameter ir mandatory" -msgstr "El parámetro projectID es obligatorio" +#: taiga/projects/history/choices.py:27 +msgid "Change" +msgstr "" -#: projects/userstories/api.py:126 projects/userstories/api.py:129 -#: projects/userstories/api.py:132 -msgid "You don't have permissions for add/modify this user story" -msgstr "No tienes permisos para crear o modificar esta historia de usuario." +#: taiga/projects/history/choices.py:28 +msgid "Create" +msgstr "" -#: projects/userstories/models.py:23 -msgid "role" -msgstr "rol" +#: taiga/projects/history/choices.py:29 +msgid "Delete" +msgstr "" -#: projects/userstories/models.py:26 projects/userstories/models.py:57 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:22 +#, python-format +msgid "%(role)s role points" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:25 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:130 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:133 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:156 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:192 +msgid "from" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:31 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:141 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:144 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:162 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:179 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:198 +msgid "to" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:43 +msgid "Added new attachment" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:61 +msgid "Updated attachment" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:67 +msgid "deprecated" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:69 +msgid "not deprecated" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:85 +msgid "Deleted attachment" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:104 +msgid "added" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:109 +msgid "removed" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +msgid "Unassigned" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:209 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:86 +msgid "-deleted-" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "to:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:20 +msgid "from:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:26 +msgid "Added" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:33 +msgid "Changed" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:40 +msgid "Deleted" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:54 +msgid "added:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:57 +msgid "removed:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:62 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:79 +msgid "From:" +msgstr "" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "To:" +msgstr "" + +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:32 +msgid "content" +msgstr "" + +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:31 +msgid "blocked note" +msgstr "" + +#: taiga/projects/issues/api.py:139 +msgid "You don't have permissions to set this sprint to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:143 +msgid "You don't have permissions to set this status to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:147 +msgid "You don't have permissions to set this severity to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:151 +msgid "You don't have permissions to set this priority to this issue." +msgstr "" + +#: taiga/projects/issues/api.py:155 +msgid "You don't have permissions to set this type to this issue." +msgstr "" + +#: taiga/projects/issues/models.py:36 taiga/projects/tasks/models.py:35 +#: taiga/projects/userstories/models.py:57 +msgid "ref" +msgstr "" + +#: taiga/projects/issues/models.py:40 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:67 +msgid "status" +msgstr "" + +#: taiga/projects/issues/models.py:42 +msgid "severity" +msgstr "" + +#: taiga/projects/issues/models.py:44 +msgid "priority" +msgstr "" + +#: taiga/projects/issues/models.py:46 +msgid "type" +msgstr "" + +#: taiga/projects/issues/models.py:49 taiga/projects/tasks/models.py:44 +#: taiga/projects/userstories/models.py:60 +msgid "milestone" +msgstr "" + +#: taiga/projects/issues/models.py:58 taiga/projects/tasks/models.py:51 +msgid "finished date" +msgstr "" + +#: taiga/projects/issues/models.py:60 taiga/projects/tasks/models.py:53 +#: taiga/projects/userstories/models.py:89 +msgid "subject" +msgstr "" + +#: taiga/projects/issues/models.py:64 taiga/projects/tasks/models.py:63 +#: taiga/projects/userstories/models.py:93 +msgid "assigned to" +msgstr "" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:67 +#: taiga/projects/userstories/models.py:103 +msgid "external reference" +msgstr "" + +#: taiga/projects/milestones/models.py:37 taiga/projects/models.py:122 +#: taiga/projects/models.py:340 taiga/projects/models.py:404 +#: taiga/projects/models.py:487 taiga/projects/models.py:545 +#: taiga/projects/wiki/models.py:30 taiga/users/models.py:182 +msgid "slug" +msgstr "" + +#: taiga/projects/milestones/models.py:42 +msgid "estimated start date" +msgstr "" + +#: taiga/projects/milestones/models.py:43 +msgid "estimated finish date" +msgstr "" + +#: taiga/projects/milestones/models.py:50 taiga/projects/models.py:344 +#: taiga/projects/models.py:408 taiga/projects/models.py:491 +msgid "is closed" +msgstr "" + +#: taiga/projects/milestones/models.py:52 +msgid "disponibility" +msgstr "" + +#: taiga/projects/milestones/validators.py:12 +msgid "There's no sprint with that id" +msgstr "" + +#: taiga/projects/mixins/blocked.py:29 +msgid "is blocked" +msgstr "" + +#: taiga/projects/mixins/ordering.py:47 +#, python-brace-format +msgid "{param} parameter is mandatory" +msgstr "" + +#: taiga/projects/mixins/ordering.py:51 +msgid "project parameter is mandatory" +msgstr "" + +#: taiga/projects/models.py:59 +msgid "email" +msgstr "" + +#: taiga/projects/models.py:61 +msgid "creado el" +msgstr "" + +#: taiga/projects/models.py:63 taiga/users/models.py:126 +msgid "token" +msgstr "" + +#: taiga/projects/models.py:69 +msgid "invitation extra text" +msgstr "" + +#: taiga/projects/models.py:75 +msgid "The user is already member of the project" +msgstr "" + +#: taiga/projects/models.py:90 +msgid "default points" +msgstr "" + +#: taiga/projects/models.py:94 +msgid "default US status" +msgstr "" + +#: taiga/projects/models.py:98 +msgid "default task status" +msgstr "" + +#: taiga/projects/models.py:101 +msgid "default priority" +msgstr "" + +#: taiga/projects/models.py:104 +msgid "default severity" +msgstr "" + +#: taiga/projects/models.py:108 +msgid "default issue status" +msgstr "" + +#: taiga/projects/models.py:112 +msgid "default issue type" +msgstr "" + +#: taiga/projects/models.py:133 +msgid "members" +msgstr "" + +#: taiga/projects/models.py:136 +msgid "total of milestones" +msgstr "" + +#: taiga/projects/models.py:137 +msgid "total story points" +msgstr "" + +#: taiga/projects/models.py:140 taiga/projects/models.py:558 +msgid "active backlog panel" +msgstr "" + +#: taiga/projects/models.py:142 taiga/projects/models.py:560 +msgid "active kanban panel" +msgstr "" + +#: taiga/projects/models.py:144 taiga/projects/models.py:562 +msgid "active wiki panel" +msgstr "" + +#: taiga/projects/models.py:146 taiga/projects/models.py:564 +msgid "active issues panel" +msgstr "" + +#: taiga/projects/models.py:149 taiga/projects/models.py:567 +msgid "videoconference system" +msgstr "" + +#: taiga/projects/models.py:151 taiga/projects/models.py:569 +msgid "videoconference room salt" +msgstr "" + +#: taiga/projects/models.py:156 +msgid "creation template" +msgstr "" + +#: taiga/projects/models.py:159 +msgid "anonymous permissions" +msgstr "" + +#: taiga/projects/models.py:163 +msgid "user permissions" +msgstr "" + +#: taiga/projects/models.py:166 +msgid "is private" +msgstr "" + +#: taiga/projects/models.py:177 +msgid "tags colors" +msgstr "" + +#: taiga/projects/models.py:327 +msgid "modules config" +msgstr "" + +#: taiga/projects/models.py:346 +msgid "is archived" +msgstr "" + +#: taiga/projects/models.py:348 taiga/projects/models.py:410 +#: taiga/projects/models.py:443 taiga/projects/models.py:466 +#: taiga/projects/models.py:493 taiga/projects/models.py:524 +#: taiga/users/models.py:113 +msgid "color" +msgstr "" + +#: taiga/projects/models.py:350 +msgid "work in progress limit" +msgstr "" + +#: taiga/projects/models.py:381 taiga/userstorage/models.py:31 +msgid "value" +msgstr "" + +#: taiga/projects/models.py:555 +msgid "default owner's role" +msgstr "" + +#: taiga/projects/models.py:571 +msgid "default options" +msgstr "" + +#: taiga/projects/models.py:572 +msgid "us statuses" +msgstr "" + +#: taiga/projects/models.py:573 taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:72 msgid "points" -msgstr "puntos" - -#: projects/userstories/models.py:65 -msgid "finish date" -msgstr "fecha de fin" - -#: projects/userstories/models.py:72 -msgid "is client requirement" -msgstr "es requisito del cliente" - -#: projects/userstories/models.py:74 -msgid "is team requirement" -msgstr "es requisito del equipo" - -#: projects/wiki/api.py:39 -msgid "You don't have permissions for add attachments to this wiki page." msgstr "" -"No tienes permisos para añadir ficheros adjuntos a esta página de wiki." -#: projects/wiki/api.py:65 -msgid "You don't haver permissions for add/modify this wiki page." -msgstr "No tienes permisos para crear or modificar esta página de wiki." +#: taiga/projects/models.py:574 +msgid "task statuses" +msgstr "" -#: settings/common.py:28 -msgid "English" -msgstr "Inglés" +#: taiga/projects/models.py:575 +msgid "issue statuses" +msgstr "" -#: settings/common.py:29 -msgid "Spanish" -msgstr "Español" +#: taiga/projects/models.py:576 +msgid "issue types" +msgstr "" + +#: taiga/projects/models.py:577 +msgid "priorities" +msgstr "" + +#: taiga/projects/models.py:578 +msgid "severities" +msgstr "" + +#: taiga/projects/models.py:579 +msgid "roles" +msgstr "" + +#: taiga/projects/notifications/choices.py:28 +msgid "Not watching" +msgstr "" + +#: taiga/projects/notifications/choices.py:29 +msgid "Watching" +msgstr "" + +#: taiga/projects/notifications/choices.py:30 +msgid "Ignoring" +msgstr "" + +#: taiga/projects/notifications/mixins.py:87 +msgid "watchers" +msgstr "" + +#: taiga/projects/notifications/models.py:57 +msgid "created date time" +msgstr "" + +#: taiga/projects/notifications/models.py:59 +msgid "updated date time" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the Wiki Page \"%(page)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the Wiki Page \"%(page)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" +msgstr "" + +#: taiga/projects/occ/mixins.py:91 +msgid "version" +msgstr "" + +#: taiga/projects/permissions.py:39 +msgid "You can't leave the project if there are no more owners" +msgstr "" + +#: taiga/projects/serializers.py:203 +msgid "Email address is already taken" +msgstr "" + +#: taiga/projects/serializers.py:215 +msgid "Invalid role for the project" +msgstr "" + +#: taiga/projects/serializers.py:370 +msgid "Default options" +msgstr "" + +#: taiga/projects/serializers.py:371 +msgid "User story's statuses" +msgstr "" + +#: taiga/projects/serializers.py:372 +msgid "Points" +msgstr "" + +#: taiga/projects/serializers.py:373 +msgid "Task's statuses" +msgstr "" + +#: taiga/projects/serializers.py:374 +msgid "Issue's statuses" +msgstr "" + +#: taiga/projects/serializers.py:375 +msgid "Issue's types" +msgstr "" + +#: taiga/projects/serializers.py:376 +msgid "Priorities" +msgstr "" + +#: taiga/projects/serializers.py:377 +msgid "Severities" +msgstr "" + +#: taiga/projects/serializers.py:378 +msgid "Roles" +msgstr "" + +#: taiga/projects/tasks/api.py:57 taiga/projects/tasks/api.py:60 +#: taiga/projects/tasks/api.py:63 taiga/projects/tasks/api.py:66 +msgid "You don't have permissions for add/modify this task." +msgstr "" + +#: taiga/projects/tasks/models.py:56 +msgid "us order" +msgstr "" + +#: taiga/projects/tasks/models.py:58 +msgid "taskboard order" +msgstr "" + +#: taiga/projects/tasks/models.py:66 +msgid "is iocaine" +msgstr "" + +#: taiga/projects/tasks/validators.py:12 +msgid "There's no task with that id" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:18 +#, python-format +msgid "" +"\n" +"

And now a few words from the jolly good fellow or sistren
" +"who thought so kindly as to invite you

\n" +"

%(extra)s

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "Accept your invitation to Taiga" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "Accept your invitation" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:26 +msgid "The Taiga Team" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"And now a few words from the jolly good fellow or sistren who thought so " +"kindly as to invite you:\n" +"\n" +"%(extra)s\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:19 +msgid "Accept your invitation to Taiga following whis link:" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:21 +msgid "" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Invitation to join to the project '%(project)s'\n" +msgstr "" + +#: taiga/projects/templates/emails/membership_notification-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[Taiga] Added to the project '%(project)s'\n" +msgstr "" + +#: taiga/projects/userstories/api.py:170 +#, python-brace-format +msgid "" +"Generating the user story [US #{ref} - {subject}](:us:{ref} \"US #{ref} - " +"{subject}\")" +msgstr "" + +#: taiga/projects/userstories/models.py:37 +msgid "role" +msgstr "" + +#: taiga/projects/userstories/models.py:75 +msgid "backlog order" +msgstr "" + +#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 +msgid "sprint order" +msgstr "" + +#: taiga/projects/userstories/models.py:87 +msgid "finish date" +msgstr "" + +#: taiga/projects/userstories/models.py:95 +msgid "is client requirement" +msgstr "" + +#: taiga/projects/userstories/models.py:97 +msgid "is team requirement" +msgstr "" + +#: taiga/projects/userstories/models.py:102 +msgid "generated from issue" +msgstr "" + +#: taiga/projects/userstories/validators.py:12 +msgid "There's no user story with that id" +msgstr "" + +#: taiga/projects/validators.py:12 +msgid "There's no project with that id" +msgstr "" + +#: taiga/projects/validators.py:21 +msgid "There's no user story status with that id" +msgstr "" + +#: taiga/projects/validators.py:30 +msgid "There's no task status with that id" +msgstr "" + +#: taiga/projects/votes/models.py:31 taiga/projects/votes/models.py:32 +#: taiga/projects/votes/models.py:54 +msgid "Votes" +msgstr "" + +#: taiga/projects/votes/models.py:50 +msgid "votes" +msgstr "" + +#: taiga/projects/votes/models.py:53 +msgid "Vote" +msgstr "" + +#: taiga/projects/wiki/api.py:60 +msgid "No content parameter" +msgstr "" + +#: taiga/projects/wiki/api.py:63 +msgid "No project_id parameter" +msgstr "" + +#: taiga/projects/wiki/models.py:36 +msgid "last modifier" +msgstr "última modificación por" + +#: taiga/projects/wiki/models.py:69 +msgid "href" +msgstr "" + +#: taiga/users/admin.py:50 +msgid "Personal info" +msgstr "Información personal" + +#: taiga/users/admin.py:52 +msgid "Permissions" +msgstr "Permisos" + +#: taiga/users/admin.py:53 +msgid "Important dates" +msgstr "" + +#: taiga/users/api.py:87 taiga/users/api.py:94 +msgid "Invalid username or email" +msgstr "Nombre de usuario o email no válidos" + +#: taiga/users/api.py:103 +msgid "Mail sended successful!" +msgstr "¡Correo enviado con éxito!" + +#: taiga/users/api.py:116 taiga/users/api.py:121 +msgid "Token is invalid" +msgstr "" + +#: taiga/users/api.py:142 +msgid "Current password parameter needed" +msgstr "" + +#: taiga/users/api.py:145 +msgid "New password parameter needed" +msgstr "" + +#: taiga/users/api.py:148 +msgid "Invalid password length at least 6 charaters needed" +msgstr "" + +#: taiga/users/api.py:151 +msgid "Invalid current password" +msgstr "" + +#: taiga/users/api.py:167 +msgid "Incomplete arguments" +msgstr "" + +#: taiga/users/api.py:172 +msgid "Invalid image format" +msgstr "Formato de imagen no válido" + +#: taiga/users/api.py:225 +msgid "Duplicated email" +msgstr "Email duplicado" + +#: taiga/users/api.py:227 +msgid "Not valid email" +msgstr "Email no válido" + +#: taiga/users/api.py:246 taiga/users/api.py:252 +msgid "" +"Invalid, are you sure the token is correct and you didn't use it before?" +msgstr "" + +#: taiga/users/api.py:279 taiga/users/api.py:287 taiga/users/api.py:290 +msgid "Invalid, are you sure the token is correct?" +msgstr "" + +#: taiga/users/models.py:69 +msgid "superuser status" +msgstr "" + +#: taiga/users/models.py:70 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" + +#: taiga/users/models.py:100 +msgid "username" +msgstr "" + +#: taiga/users/models.py:101 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" +msgstr "" + +#: taiga/users/models.py:104 +msgid "Enter a valid username." +msgstr "" + +#: taiga/users/models.py:107 +msgid "active" +msgstr "" + +#: taiga/users/models.py:108 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" + +#: taiga/users/models.py:114 +msgid "biography" +msgstr "" + +#: taiga/users/models.py:117 +msgid "photo" +msgstr "" + +#: taiga/users/models.py:118 +msgid "date joined" +msgstr "" + +#: taiga/users/models.py:120 +msgid "default language" +msgstr "idioma por defecto" + +#: taiga/users/models.py:122 +msgid "default timezone" +msgstr "zona horaria por defecto" + +#: taiga/users/models.py:124 +msgid "colorize tags" +msgstr "añade color a las etiquetas" + +#: taiga/users/models.py:129 +msgid "email token" +msgstr "" + +#: taiga/users/models.py:131 +msgid "new email address" +msgstr "nueva dirección de email" + +#: taiga/users/models.py:185 +msgid "permissions" +msgstr "permisos" + +#: taiga/users/serializers.py:52 +msgid "invalid username" +msgstr "nombre de usuario no válido" + +#: taiga/users/serializers.py:53 +msgid "invalid" +msgstr "no válido" + +#: taiga/users/serializers.py:58 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "" + +#: taiga/users/serializers.py:64 +msgid "Invalid username. Try with a different one." +msgstr "" + +#: taiga/users/templates/emails/change_email-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Change your email

\n" +"

Hello %(full_name)s,
please confirm your email

\n" +" Confirm " +"email\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/users/templates/emails/change_email-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, please confirm your email\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "" + +#: taiga/users/templates/emails/password_recovery-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(full_name)s, you asked to recover your password\n" +"\n" +"%(url)s\n" +"\n" +"You can ignore this message if you did not request.\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/password_recovery-subject.jinja:1 +msgid "[Taiga] Password recovery" +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:6 +msgid "" +"\n" +" \n" +"

Thank you for registering in Taiga

\n" +"

We hope you enjoy it

\n" +"

We built Taiga because we wanted the project management tool " +"that sits open on our computers all day long, to serve as a continued " +"reminder of why we love to collaborate, code and design.

\n" +"

We built it to be beautiful, elegant, simple to use and fun - " +"without forsaking flexibility and power.

\n" +" The taiga Team\n" +" You may remove your account from this service clicking " +"here\n" +" " +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:1 +msgid "" +"\n" +"Thank you for registering in Taiga\n" +"\n" +"We hope you enjoy it\n" +"\n" +"We built Taiga because we wanted the project management tool that sits open " +"on our computers all day long, to serve as a continued reminder of why we " +"love to collaborate, code and design.\n" +"\n" +"We built it to be beautiful, elegant, simple to use and fun - without " +"forsaking flexibility and power.\n" +"\n" +"--\n" +"The taiga Team\n" +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-text.jinja:13 +#, python-format +msgid "" +"\n" +"You may remove your account from this service: %(url)s\n" +msgstr "" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "¡Te hemos Taigatizado!" + +#: taiga/users/validators.py:29 +msgid "There's no role with that id" +msgstr "" + +#: taiga/userstorage/api.py:50 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" + +#: taiga/userstorage/models.py:30 +msgid "key" +msgstr "" + +#: taiga/webhooks/models.py:28 taiga/webhooks/models.py:38 +msgid "URL" +msgstr "URL" + +#: taiga/webhooks/models.py:29 +msgid "secret key" +msgstr "" + +#: taiga/webhooks/models.py:39 +msgid "Status code" +msgstr "" + +#: taiga/webhooks/models.py:40 +msgid "Request data" +msgstr "" + +#: taiga/webhooks/models.py:41 +msgid "Request headers" +msgstr "" + +#: taiga/webhooks/models.py:42 +msgid "Response data" +msgstr "" + +#: taiga/webhooks/models.py:43 +msgid "Response headers" +msgstr "" + +#: taiga/webhooks/models.py:44 +msgid "Duration" +msgstr "Duración" From 2d960c7a5dbfc77034a5fb8324609eb4709164c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 26 Mar 2015 13:42:58 +0100 Subject: [PATCH 15/96] i18n in emails --- taiga/auth/services.py | 2 +- taiga/base/management/commands/test_emails.py | 24 +++++++++++++++---- taiga/export_import/tasks.py | 8 +++---- taiga/projects/notifications/services.py | 1 + taiga/projects/services/invitations.py | 3 ++- taiga/users/admin.py | 2 +- taiga/users/api.py | 5 ++-- taiga/users/fixtures/initial_user.json | 4 ++-- .../migrations/0009_auto_20150326_1241.py | 24 +++++++++++++++++++ taiga/users/models.py | 12 +++++----- taiga/users/serializers.py | 8 +++---- tests/integration/test_memberships.py | 2 +- 12 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 taiga/users/migrations/0009_auto_20150326_1241.py diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 72589eac..33a82baa 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -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()) diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py index 29752dd4..da535c7f 100644 --- a/taiga/base/management/commands/test_emails.py +++ b/taiga/base/management/commands/test_emails.py @@ -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 = '' + 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 ") 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], diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index 635eb63e..29032861 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -49,7 +49,7 @@ def dump_project(self, user, project): "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 +60,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() @@ -81,10 +81,10 @@ def load_project_dump(user, 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() diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 189d4f6a..b066da34 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -269,6 +269,7 @@ def send_sync_notifications(notification_id): for user in notification.notify_users.distinct(): context["user"] = user + context["lang"] = user.lang email.send(user.email, context) notification.delete() diff --git a/taiga/projects/services/invitations.py b/taiga/projects/services/invitations.py index 1d79e58d..4196612c 100644 --- a/taiga/projects/services/invitations.py +++ b/taiga/projects/services/invitations.py @@ -9,10 +9,11 @@ def send_invitation(invitation): mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) if invitation.user: template = mbuilder.membership_notification + email = template(invitation.user, {"membership": invitation}) else: template = mbuilder.membership_invitation + email = template(invitation.email, {"membership": invitation}) - email = template(invitation.email, {"membership": invitation}) email.send() diff --git a/taiga/users/admin.py b/taiga/users/admin.py index a3452616..b2cd50cf 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -48,7 +48,7 @@ class UserAdmin(DjangoUserAdmin): fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), - (_('Extra info'), {'fields': ('color', 'default_language', 'default_timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}), + (_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}), (_('Permissions'), {'fields': ('is_active', 'is_superuser',)}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) diff --git a/taiga/users/api.py b/taiga/users/api.py index 93f79be3..36dff9b1 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -97,7 +97,7 @@ class UsersViewSet(ModelCrudViewSet): user.save(update_fields=["token"]) mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - email = mbuilder.password_recovery(user.email, {"user": user}) + email = mbuilder.password_recovery(user, {"user": user}) email.send() return response.Ok({"detail": _("Mail sended successful!"), @@ -231,7 +231,8 @@ class UsersViewSet(ModelCrudViewSet): request.user.new_email = new_email request.user.save(update_fields=["email_token", "new_email"]) mbuilder = MagicMailBuilder(template_mail_cls=InlineCSSTemplateMail) - email = mbuilder.change_email(request.user.new_email, {"user": request.user}) + email = mbuilder.change_email(request.user.new_email, {"user": request.user, + "lang": request.user.lang}) email.send() return ret diff --git a/taiga/users/fixtures/initial_user.json b/taiga/users/fixtures/initial_user.json index e13bcb95..ed7833c1 100644 --- a/taiga/users/fixtures/initial_user.json +++ b/taiga/users/fixtures/initial_user.json @@ -5,12 +5,12 @@ "username": "admin", "full_name": "", "bio": "", - "default_language": "", + "lang": "", "color": "", "photo": "", "is_active": true, "colorize_tags": false, - "default_timezone": "", + "timezone": "", "is_superuser": true, "token": "", "last_login": "2013-04-04T07:36:09.880Z", diff --git a/taiga/users/migrations/0009_auto_20150326_1241.py b/taiga/users/migrations/0009_auto_20150326_1241.py new file mode 100644 index 00000000..d12b59f8 --- /dev/null +++ b/taiga/users/migrations/0009_auto_20150326_1241.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0008_auto_20150213_1701'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='default_language', + new_name='lang', + ), + migrations.RenameField( + model_name='user', + old_name='default_timezone', + new_name='timezone', + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index e2ffe491..3c4af001 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -116,10 +116,10 @@ class User(AbstractBaseUser, PermissionsMixin): max_length=500, null=True, blank=True, verbose_name=_("photo")) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) - default_language = models.CharField(max_length=20, null=False, blank=True, default="", - verbose_name=_("default language")) - default_timezone = models.CharField(max_length=20, null=False, blank=True, default="", - verbose_name=_("default timezone")) + lang = models.CharField(max_length=20, null=False, blank=True, default="", + verbose_name=_("default language")) + timezone = models.CharField(max_length=20, null=False, blank=True, default="", + verbose_name=_("default timezone")) colorize_tags = models.BooleanField(null=False, blank=True, default=False, verbose_name=_("colorize tags")) token = models.CharField(max_length=200, null=True, blank=True, default=None, @@ -166,8 +166,8 @@ class User(AbstractBaseUser, PermissionsMixin): self.full_name = "Deleted user" self.color = "" self.bio = "" - self.default_language = "" - self.default_timezone = "" + self.lang = "" + self.timezone = "" self.colorize_tags = True self.token = None self.set_unusable_password() diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 5bb0d1ff..63173f34 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -43,8 +43,8 @@ class UserSerializer(ModelSerializer): # IMPORTANT: Maintain the UserAdminSerializer Meta up to date # with this info (including there the email) fields = ("id", "username", "full_name", "full_name_display", - "color", "bio", "default_language", - "default_timezone", "is_active", "photo", "big_photo") + "color", "bio", "lang", "timezone", "is_active", + "photo", "big_photo") read_only_fields = ("id",) def validate_username(self, attrs, source): @@ -81,8 +81,8 @@ class UserAdminSerializer(UserSerializer): # IMPORTANT: Maintain the UserSerializer Meta up to date # with this info (including here the email) fields = ("id", "username", "full_name", "full_name_display", "email", - "color", "bio", "default_language", - "default_timezone", "is_active", "photo", "big_photo") + "color", "bio", "lang", "timezone", "is_active", "photo", + "big_photo") read_only_fields = ("id", "email") diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index 9805006d..75d7f29a 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -80,7 +80,7 @@ def test_api_create_bulk_members_with_extra_text(client, outbox): def test_api_resend_invitation(client, outbox): - invitation = f.create_invitation() + invitation = f.create_invitation(user=None) f.MembershipFactory(project=invitation.project, user=invitation.project.owner, is_owner=True) url = reverse("memberships-resend-invitation", kwargs={"pk": invitation.pk}) From d3fade95650ba10db2f905e55562bdfc99d3999c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 27 Mar 2015 13:31:36 +0100 Subject: [PATCH 16/96] Add gettext to translatable strings --- locale/en/LC_MESSAGES/django.po | 2 +- taiga/auth/api.py | 2 +- taiga/auth/serializers.py | 7 +- taiga/auth/services.py | 6 +- taiga/auth/tokens.py | 5 +- taiga/base/api/generics.py | 60 ++-- taiga/base/api/mixins.py | 11 +- taiga/base/api/permissions.py | 3 +- taiga/base/api/views.py | 15 +- taiga/base/api/viewsets.py | 11 +- taiga/base/filters.py | 21 +- taiga/base/utils/signals.py | 4 +- taiga/export_import/api.py | 2 +- taiga/export_import/dump_service.py | 26 +- taiga/export_import/serializers.py | 7 +- taiga/export_import/tasks.py | 9 +- taiga/hooks/api.py | 2 +- taiga/hooks/bitbucket/event_hooks.py | 7 +- taiga/hooks/github/event_hooks.py | 10 +- taiga/hooks/gitlab/event_hooks.py | 6 +- taiga/locale/en/LC_MESSAGES/django.po | 372 +++++++++++++++++++-- taiga/projects/api.py | 6 +- taiga/projects/attachments/api.py | 12 +- taiga/projects/choices.py | 7 +- taiga/projects/history/api.py | 5 +- taiga/projects/history/mixins.py | 2 +- taiga/projects/issues/api.py | 2 +- taiga/projects/milestones/models.py | 2 +- taiga/projects/milestones/serializers.py | 7 +- taiga/projects/milestones/validators.py | 2 +- taiga/projects/mixins/ordering.py | 2 +- taiga/projects/notifications/models.py | 12 +- taiga/projects/notifications/services.py | 9 +- taiga/projects/notifications/validators.py | 4 +- taiga/projects/occ/mixins.py | 6 +- taiga/projects/permissions.py | 2 +- taiga/projects/serializers.py | 12 +- taiga/projects/services/stats.py | 10 +- taiga/projects/tasks/api.py | 2 +- taiga/projects/tasks/validators.py | 2 +- taiga/projects/userstories/validators.py | 4 +- taiga/projects/validators.py | 2 +- taiga/projects/wiki/api.py | 2 +- taiga/users/api.py | 2 +- taiga/users/services.py | 15 +- taiga/users/validators.py | 2 +- taiga/userstorage/api.py | 2 +- taiga/webhooks/models.py | 12 +- 48 files changed, 524 insertions(+), 211 deletions(-) diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 8b986771..6a22a978 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"POT-Creation-Date: 2015-04-06 17:04+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" diff --git a/taiga/auth/api.py b/taiga/auth/api.py index 48c1041d..717269ae 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -17,7 +17,7 @@ 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 diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py index 8fbba177..23789656 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/serializers.py @@ -18,6 +18,7 @@ from rest_framework import serializers from django.core import validators from django.core.exceptions import ValidationError +from django.utils.translation import ugettext as _ import re @@ -29,13 +30,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 diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 33a82baa..0498bddd 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -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 register.")) 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 diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py index 75d36e21..f113ba8a 100644 --- a/taiga/auth/tokens.py +++ b/taiga/auth/tokens.py @@ -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 diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index a5b40c18..9536cb04 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -126,11 +126,11 @@ class GenericAPIView(views.APIView): """ 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.', + 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: @@ -141,12 +141,10 @@ class GenericAPIView(views.APIView): 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 - ) + 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) @@ -191,10 +189,10 @@ class GenericAPIView(views.APIView): """ filter_backends = self.filter_backends or [] if not filter_backends and hasattr(self, 'filter_backend'): - raise RuntimeError('The `filter_backend` attribute and `FILTER_BACKEND` setting ' - 'are due to be deprecated in favor of a `filter_backends` ' - 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' - 'a *list* of filter backend classes.') + raise RuntimeError(_('The `filter_backend` attribute and `FILTER_BACKEND` setting ' + 'are due to be deprecated in favor of a `filter_backends` ' + 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' + 'a *list* of filter backend classes.')) return filter_backends ########################################################### @@ -212,8 +210,8 @@ class GenericAPIView(views.APIView): 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.') + raise RuntimeError(_('The `queryset` parameter to `get_paginate_by()` ' + 'is due to be deprecated.')) if self.paginate_by_param: try: return strict_positive_int( @@ -233,11 +231,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 +257,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 +285,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 diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index ed2b2f24..d28c8d06 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -22,6 +22,7 @@ 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 @@ -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) diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py index ff1f8bef..c4e6917d 100644 --- a/taiga/base/api/permissions.py +++ b/taiga/base/api/permissions.py @@ -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) diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py index 26cc5970..3656024e 100644 --- a/taiga/base/api/views.py +++ b/taiga/base/api/views.py @@ -23,6 +23,7 @@ from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponse from django.utils.datastructures import SortedDict from django.views.decorators.csrf import csrf_exempt +from django.utils.translation import ugettext as _ from rest_framework import status, exceptions from rest_framework.compat import smart_text, HttpResponseBase, View @@ -93,10 +94,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 @@ -345,11 +346,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 +445,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) diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py index cad36dcd..82d29ef1 100644 --- a/taiga/base/api/viewsets.py +++ b/taiga/base/api/viewsets.py @@ -19,6 +19,7 @@ from functools import update_wrapper from django.utils.decorators import classonlymethod +from django.utils.translation import ugettext as _ from . import views from . import mixins @@ -53,12 +54,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) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 5f7c50e8..5ae4434b 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -19,7 +19,7 @@ import logging from django.apps import apps from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework import filters @@ -60,7 +60,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 +104,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 +193,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 +250,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') diff --git a/taiga/base/utils/signals.py b/taiga/base/utils/signals.py index 64cc580a..0c326c95 100644 --- a/taiga/base/utils/signals.py +++ b/taiga/base/utils/signals.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 8ddb4c15..01947685 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -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 diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 2ef615a1..13b09a4c 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 attributes')) 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) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 6287e78d..1805c465 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -18,11 +18,12 @@ 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 @@ -153,7 +154,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 +459,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 diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index 29032861..9ac5b42f 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -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,8 +46,8 @@ 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, ctx) @@ -78,8 +79,8 @@ 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, ctx) email.send() diff --git a/taiga/hooks/api.py b/taiga/hooks/api.py index a95d6fbe..d4345cbc 100644 --- a/taiga/hooks/api.py +++ b/taiga/hooks/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py index d82d3d4e..7461eabb 100644 --- a/taiga/hooks/bitbucket/event_hooks.py +++ b/taiga/hooks/bitbucket/event_hooks.py @@ -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) diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py index 50c465c5..6ccac3da 100644 --- a/taiga/hooks/github/event_hooks.py +++ b/taiga/hooks/github/event_hooks.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus @@ -85,7 +85,7 @@ class PushEventHook(BaseEventHook): element.save() snapshot = take_snapshot(element, - comment="Status changed from GitHub commit", + comment=_("Status changed from GitHub commit"), user=get_github_user(github_user)) send_notifications(element, history=snapshot) @@ -93,7 +93,7 @@ class PushEventHook(BaseEventHook): def replace_github_references(project_url, wiki_text): if wiki_text == None: wiki_text = "" - + template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) @@ -125,7 +125,7 @@ class IssuesEventHook(BaseEventHook): ) take_snapshot(issue, user=get_github_user(github_user)) - snapshot = take_snapshot(issue, comment="Created from GitHub", user=get_github_user(github_user)) + snapshot = take_snapshot(issue, comment=_("Created from GitHub"), user=get_github_user(github_user)) send_notifications(issue, history=snapshot) @@ -149,6 +149,6 @@ class IssueCommentEventHook(BaseEventHook): for item in list(issues) + list(tasks) + list(uss): snapshot = take_snapshot(item, - comment="From GitHub:\n\n{}".format(comment_message), + comment=_("From GitHub:\n\n{}".format(comment_message)), user=get_github_user(github_user)) send_notifications(item, history=snapshot) diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py index faa81df1..8776d8c7 100644 --- a/taiga/hooks/gitlab/event_hooks.py +++ b/taiga/hooks/gitlab/event_hooks.py @@ -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) diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index 742c8bcd..0f1631d5 100644 --- a/taiga/locale/en/LC_MESSAGES/django.po +++ b/taiga/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-03-25 20:09+0100\n" +"POT-Creation-Date: 2015-04-06 17:04+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -28,6 +28,15 @@ msgstr "" msgid "invalid login type" msgstr "" +#: taiga/auth/serializers.py:33 taiga/users/serializers.py:52 +msgid "invalid username" +msgstr "" + +#: taiga/auth/serializers.py:38 taiga/users/serializers.py:58 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "" + #: taiga/auth/services.py:75 msgid "Username is already in use." msgstr "" @@ -36,19 +45,129 @@ msgstr "" msgid "Email is already in use." msgstr "" +#: taiga/auth/services.py:94 +msgid "Token not matches any valid invitation." +msgstr "" + +#: taiga/auth/services.py:122 +msgid "User is already register." +msgstr "" + +#: taiga/auth/services.py:146 +msgid "Membership with user is already exists." +msgstr "" + #: taiga/auth/services.py:172 msgid "Error on creating new user." msgstr "" -#: taiga/base/api/generics.py:162 +#: taiga/auth/tokens.py:47 taiga/auth/tokens.py:54 +msgid "Invalid token" +msgstr "" + +#: taiga/base/api/generics.py:129 +msgid "" +"The `page_size` parameter to `paginate_queryset()` is due to be deprecated. " +"Note that the return style of this method is also changed, and will simply " +"return a page object when called without a `page_size` argument." +msgstr "" + +#: taiga/base/api/generics.py:144 taiga/base/api/mixins.py:98 +msgid "" +"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." +msgstr "" + +#: taiga/base/api/generics.py:160 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" -#: taiga/base/api/generics.py:166 +#: taiga/base/api/generics.py:164 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "" +#: taiga/base/api/generics.py:192 +msgid "" +"The `filter_backend` attribute and `FILTER_BACKEND` setting are due to be " +"deprecated in favor of a `filter_backends` attribute and " +"`DEFAULT_FILTER_BACKENDS` setting, that take a *list* of filter backend " +"classes." +msgstr "" + +#: taiga/base/api/generics.py:213 +msgid "" +"The `queryset` parameter to `get_paginate_by()` is due to be deprecated." +msgstr "" + +#: taiga/base/api/generics.py:234 +#, python-format +msgid "" +"'%s' should either include a 'serializer_class' attribute, or use the " +"'model' attribute as a shortcut for automatically generating a serializer " +"class." +msgstr "" + +#: taiga/base/api/generics.py:260 +#, python-format +msgid "'%s' must define 'queryset' or 'model'" +msgstr "" + +#: taiga/base/api/generics.py:288 +msgid "" +"The `pk_url_kwarg` attribute is due to be deprecated. Use the `lookup_field` " +"attribute instead" +msgstr "" + +#: taiga/base/api/generics.py:291 +msgid "" +"The `slug_url_kwarg` attribute is due to be deprecated. Use the " +"`lookup_field` attribute instead" +msgstr "" + +#: taiga/base/api/generics.py:294 +#, python-format +msgid "" +"Expected view %s to be called with a URL keyword argument named \"%s\". Fix " +"your URL conf, or set the `.lookup_field` attribute on the view correctly." +msgstr "" + +#: taiga/base/api/permissions.py:61 +msgid "Invalid permission definition." +msgstr "" + +#: taiga/base/api/views.py:97 +msgid "Not found" +msgstr "" + +#: taiga/base/api/views.py:100 +msgid "Permission denied" +msgstr "" + +#: taiga/base/api/views.py:349 +#, python-format +msgid "" +"Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be " +"returned from the view, but received a `%s`" +msgstr "" + +#: taiga/base/api/views.py:448 +msgid "Server application error" +msgstr "" + +#: taiga/base/api/viewsets.py:57 +#, python-format +msgid "" +"You tried to pass in the %s method name as a keyword argument to %s(). Don't " +"do that." +msgstr "" + +#: taiga/base/api/viewsets.py:61 +#, python-format +msgid "%s() received an invalid keyword %r" +msgstr "" + #: taiga/base/connectors/exceptions.py:24 msgid "Connection error." msgstr "" @@ -81,6 +200,20 @@ msgstr "" msgid "Precondition error" msgstr "" +#: taiga/base/filters.py:63 +msgid "Error in filter params types." +msgstr "" + +#: taiga/base/filters.py:107 taiga/base/filters.py:196 +#: taiga/base/filters.py:253 +msgid "Filtering project diferent value than an integer: {}" +msgstr "" + +#: taiga/base/filters.py:110 taiga/base/filters.py:199 +#: taiga/base/filters.py:255 +msgid "'project' must be an integer value." +msgstr "" + #: taiga/base/tags.py:25 msgid "tags" msgstr "" @@ -166,6 +299,10 @@ msgid "" " " msgstr "" +#: taiga/base/utils/signals.py:27 +msgid "The parameters must be lists of at least one parameter (the signal)." +msgstr "" + #: taiga/export_import/api.py:183 msgid "Needed dump file" msgstr "" @@ -174,16 +311,80 @@ msgstr "" msgid "Invalid dump format" msgstr "" -#: taiga/export_import/serializers.py:377 +#: taiga/export_import/dump_service.py:88 +msgid "error importing project" +msgstr "" + +#: taiga/export_import/dump_service.py:101 +msgid "error importing choices" +msgstr "" + +#: taiga/export_import/dump_service.py:106 +msgid "error importing default choices" +msgstr "" + +#: taiga/export_import/dump_service.py:116 +msgid "error importing custom attributes" +msgstr "" + +#: taiga/export_import/dump_service.py:121 +msgid "error importing roles" +msgstr "" + +#: taiga/export_import/dump_service.py:136 +msgid "error importing memberships" +msgstr "" + +#: taiga/export_import/dump_service.py:141 +msgid "error importing milestones" +msgstr "" + +#: taiga/export_import/dump_service.py:146 +msgid "error importing wiki pages" +msgstr "" + +#: taiga/export_import/dump_service.py:151 +msgid "error importing wiki links" +msgstr "" + +#: taiga/export_import/dump_service.py:156 +#: taiga/export_import/dump_service.py:166 +msgid "error importing issues" +msgstr "" + +#: taiga/export_import/dump_service.py:161 +msgid "error importing user stories" +msgstr "" + +#: taiga/export_import/serializers.py:157 +msgid "{}=\"{}\" not found in this project" +msgstr "" + +#: taiga/export_import/serializers.py:378 #: taiga/projects/custom_attributes/serializers.py:104 msgid "Invalid content. It must be {\"key\": \"value\",...}" msgstr "" -#: taiga/export_import/serializers.py:392 +#: taiga/export_import/serializers.py:393 #: taiga/projects/custom_attributes/serializers.py:119 msgid "It contain invalid custom fields." msgstr "" +#: taiga/export_import/serializers.py:462 +#: taiga/projects/milestones/serializers.py:63 +#: taiga/projects/serializers.py:64 taiga/projects/serializers.py:88 +#: taiga/projects/serializers.py:110 taiga/projects/serializers.py:142 +msgid "Name duplicated for the project" +msgstr "" + +#: taiga/export_import/tasks.py:49 taiga/export_import/tasks.py:50 +msgid "Error generating project dump" +msgstr "" + +#: taiga/export_import/tasks.py:82 taiga/export_import/tasks.py:83 +msgid "Error loading project dump" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-subject.jinja:1 #, python-format msgid "[%(project)s] Your project dump has been generated" @@ -286,28 +487,55 @@ msgstr "" msgid "The payload is not a valid application/x-www-form-urlencoded" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:46 +#: taiga/hooks/bitbucket/event_hooks.py:45 msgid "The payload is not valid" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:82 +#: taiga/hooks/bitbucket/event_hooks.py:81 #: taiga/hooks/github/event_hooks.py:75 taiga/hooks/gitlab/event_hooks.py:74 msgid "The referenced element doesn't exist" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:89 +#: taiga/hooks/bitbucket/event_hooks.py:88 #: taiga/hooks/github/event_hooks.py:82 taiga/hooks/gitlab/event_hooks.py:81 msgid "The status doesn't exist" msgstr "" +#: taiga/hooks/bitbucket/event_hooks.py:94 +msgid "Status changed from BitBucket commit" +msgstr "" + +#: taiga/hooks/github/event_hooks.py:88 +msgid "Status changed from GitHub commit" +msgstr "" + #: taiga/hooks/github/event_hooks.py:113 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" msgstr "" +#: taiga/hooks/github/event_hooks.py:128 +msgid "Created from GitHub" +msgstr "" + #: taiga/hooks/github/event_hooks.py:135 taiga/hooks/github/event_hooks.py:144 msgid "Invalid issue comment information" msgstr "" +#: taiga/hooks/github/event_hooks.py:152 +msgid "" +"From GitHub:\n" +"\n" +"{}" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:87 +msgid "Status changed from GitLab commit" +msgstr "" + +#: taiga/hooks/gitlab/event_hooks.py:129 +msgid "Created from GitLab" +msgstr "" + #: taiga/permissions/permissions.py:21 taiga/permissions/permissions.py:31 #: taiga/permissions/permissions.py:52 msgid "View project" @@ -470,6 +698,14 @@ msgstr "" msgid "Admin roles" msgstr "" +#: taiga/projects/api.py:189 +msgid "Not valid template name" +msgstr "" + +#: taiga/projects/api.py:192 +msgid "Not valid template description" +msgstr "" + #: taiga/projects/api.py:454 taiga/projects/serializers.py:227 msgid "At least one of the user must be an active admin" msgstr "" @@ -478,10 +714,19 @@ msgstr "" msgid "You don't have permisions to see that." msgstr "" +#: taiga/projects/attachments/api.py:47 +msgid "Non partial updates not supported" +msgstr "" + +#: taiga/projects/attachments/api.py:62 +msgid "Project ID not matches between object and project" +msgstr "" + #: taiga/projects/attachments/models.py:54 taiga/projects/issues/models.py:38 #: taiga/projects/milestones/models.py:39 taiga/projects/models.py:131 -#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:64 -#: taiga/projects/wiki/models.py:34 taiga/userstorage/models.py:25 +#: taiga/projects/notifications/models.py:57 taiga/projects/tasks/models.py:37 +#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:34 +#: taiga/userstorage/models.py:25 msgid "owner" msgstr "" @@ -492,9 +737,9 @@ msgstr "" #: taiga/projects/models.py:383 taiga/projects/models.py:412 #: taiga/projects/models.py:445 taiga/projects/models.py:468 #: taiga/projects/models.py:495 taiga/projects/models.py:526 -#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:62 -#: taiga/projects/wiki/models.py:28 taiga/projects/wiki/models.py:66 -#: taiga/users/models.py:193 +#: taiga/projects/notifications/models.py:69 taiga/projects/tasks/models.py:41 +#: taiga/projects/userstories/models.py:62 taiga/projects/wiki/models.py:28 +#: taiga/projects/wiki/models.py:66 taiga/users/models.py:193 msgid "project" msgstr "" @@ -542,6 +787,14 @@ msgstr "" msgid "order" msgstr "" +#: taiga/projects/choices.py:21 +msgid "AppearIn" +msgstr "" + +#: taiga/projects/choices.py:22 +msgid "Talky" +msgstr "" + #: taiga/projects/custom_attributes/models.py:31 #: taiga/projects/milestones/models.py:34 taiga/projects/models.py:120 #: taiga/projects/models.py:338 taiga/projects/models.py:377 @@ -573,6 +826,14 @@ msgstr "" msgid "Already exists one with the same name." msgstr "" +#: taiga/projects/history/api.py:70 +msgid "Comment already deleted" +msgstr "" + +#: taiga/projects/history/api.py:89 +msgid "Comment not deleted" +msgstr "" + #: taiga/projects/history/choices.py:27 msgid "Change" msgstr "" @@ -637,6 +898,7 @@ msgstr "" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +#: taiga/projects/services/stats.py:124 taiga/projects/services/stats.py:125 msgid "Unassigned" msgstr "" @@ -783,6 +1045,10 @@ msgstr "" msgid "disponibility" msgstr "" +#: taiga/projects/milestones/models.py:75 +msgid "The estimated start must be previous to the estimated finish." +msgstr "" + #: taiga/projects/milestones/validators.py:12 msgid "There's no sprint with that id" msgstr "" @@ -984,14 +1250,27 @@ msgstr "" msgid "watchers" msgstr "" -#: taiga/projects/notifications/models.py:57 +#: taiga/projects/notifications/models.py:59 msgid "created date time" msgstr "" -#: taiga/projects/notifications/models.py:59 +#: taiga/projects/notifications/models.py:61 msgid "updated date time" msgstr "" +#: taiga/projects/notifications/models.py:63 +msgid "history entries" +msgstr "" + +#: taiga/projects/notifications/models.py:66 +msgid "notify users" +msgstr "" + +#: taiga/projects/notifications/services.py:63 +#: taiga/projects/notifications/services.py:77 +msgid "Notify exists for specified user and project" +msgstr "" + #: taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja:1 #, python-format msgid "" @@ -1013,6 +1292,22 @@ msgid "" "[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" msgstr "" +#: taiga/projects/notifications/validators.py:43 +msgid "Watchers contains invalid users" +msgstr "" + +#: taiga/projects/occ/mixins.py:34 +msgid "The version must be an integer" +msgstr "" + +#: taiga/projects/occ/mixins.py:55 +msgid "The version is not valid" +msgstr "" + +#: taiga/projects/occ/mixins.py:71 +msgid "The version doesn't match with the current one" +msgstr "" + #: taiga/projects/occ/mixins.py:91 msgid "version" msgstr "" @@ -1029,6 +1324,10 @@ msgstr "" msgid "Invalid role for the project" msgstr "" +#: taiga/projects/serializers.py:313 +msgid "Total milestones must be major or equal to zero" +msgstr "" + #: taiga/projects/serializers.py:370 msgid "Default options" msgstr "" @@ -1065,6 +1364,14 @@ msgstr "" msgid "Roles" msgstr "" +#: taiga/projects/services/stats.py:72 +msgid "Future sprint" +msgstr "" + +#: taiga/projects/services/stats.py:89 +msgid "Project End" +msgstr "" + #: taiga/projects/tasks/api.py:57 taiga/projects/tasks/api.py:60 #: taiga/projects/tasks/api.py:63 taiga/projects/tasks/api.py:66 msgid "You don't have permissions for add/modify this task." @@ -1189,15 +1496,15 @@ msgstr "" msgid "There's no user story with that id" msgstr "" -#: taiga/projects/validators.py:12 +#: taiga/projects/validators.py:28 msgid "There's no project with that id" msgstr "" -#: taiga/projects/validators.py:21 +#: taiga/projects/validators.py:37 msgid "There's no user story status with that id" msgstr "" -#: taiga/projects/validators.py:30 +#: taiga/projects/validators.py:46 msgid "There's no task status with that id" msgstr "" @@ -1286,12 +1593,12 @@ msgstr "" msgid "Not valid email" msgstr "" -#: taiga/users/api.py:246 taiga/users/api.py:252 +#: taiga/users/api.py:247 taiga/users/api.py:253 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" -#: taiga/users/api.py:279 taiga/users/api.py:287 taiga/users/api.py:290 +#: taiga/users/api.py:280 taiga/users/api.py:288 taiga/users/api.py:291 msgid "Invalid, are you sure the token is correct?" msgstr "" @@ -1364,23 +1671,18 @@ msgstr "" msgid "permissions" msgstr "" -#: taiga/users/serializers.py:52 -msgid "invalid username" -msgstr "" - #: taiga/users/serializers.py:53 msgid "invalid" msgstr "" -#: taiga/users/serializers.py:58 -msgid "" -"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" -msgstr "" - #: taiga/users/serializers.py:64 msgid "Invalid username. Try with a different one." msgstr "" +#: taiga/users/services.py:48 taiga/users/services.py:52 +msgid "Username or password does not matches user." +msgstr "" + #: taiga/users/templates/emails/change_email-body-html.jinja:4 #, python-format msgid "" @@ -1507,25 +1809,25 @@ msgid "secret key" msgstr "" #: taiga/webhooks/models.py:39 -msgid "Status code" +msgid "status code" msgstr "" #: taiga/webhooks/models.py:40 -msgid "Request data" +msgid "request data" msgstr "" #: taiga/webhooks/models.py:41 -msgid "Request headers" +msgid "request headers" msgstr "" #: taiga/webhooks/models.py:42 -msgid "Response data" +msgid "response data" msgstr "" #: taiga/webhooks/models.py:43 -msgid "Response headers" +msgid "response headers" msgstr "" #: taiga/webhooks/models.py:44 -msgid "Duration" +msgid "duration" msgstr "" diff --git a/taiga/projects/api.py b/taiga/projects/api.py index f7317873..f0695984 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -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) diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 383dcbba..0d26a0a8 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -14,24 +14,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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) diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py index d1c9171b..4ab6f2e0 100644 --- a/taiga/projects/choices.py +++ b/taiga/projects/choices.py @@ -14,7 +14,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.utils.translation import ugettext_lazy as _ + + VIDEOCONFERENCES_CHOICES = ( - ("appear-in", "AppearIn"), - ("talky", "Talky"), + ("appear-in", _("AppearIn")), + ("talky", _("Talky")), ) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 49023f94..2d6c365b 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -15,6 +15,7 @@ # along with this program. If not, see . 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 diff --git a/taiga/projects/history/mixins.py b/taiga/projects/history/mixins.py index 573f778f..9e379f77 100644 --- a/taiga/projects/history/mixins.py +++ b/taiga/projects/history/mixins.py @@ -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 diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index d51f6686..3a6133d6 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index 22a9b179..7e18048e 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -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: diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index e333c71f..aa46539d 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -14,15 +14,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import json +from django.utils.translation import ugettext as _ from rest_framework 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 diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py index 7add2199..2e767b3e 100644 --- a/taiga/projects/milestones/validators.py +++ b/taiga/projects/milestones/validators.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework import serializers diff --git a/taiga/projects/mixins/ordering.py b/taiga/projects/mixins/ordering.py index e9438d27..b818a25e 100644 --- a/taiga/projects/mixins/ordering.py +++ b/taiga/projects/mixins/ordering.py @@ -15,7 +15,7 @@ # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from taiga.base import response from taiga.base import exceptions as exc diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py index 6b631abc..29983f90 100644 --- a/taiga/projects/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -18,9 +18,11 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from .choices import NOTIFY_LEVEL_CHOICES from taiga.projects.history.choices import HISTORY_TYPE_CHOICES +from .choices import NOTIFY_LEVEL_CHOICES + + class NotifyPolicy(models.Model): """ This class represents a persistence for @@ -52,19 +54,19 @@ class HistoryChangeNotification(models.Model): """ key = models.CharField(max_length=255, unique=False, editable=False) owner = models.ForeignKey("users.User", null=False, blank=False, - verbose_name="owner",related_name="+") + verbose_name=_("owner"), related_name="+") created_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, verbose_name=_("created date time")) updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, verbose_name=_("updated date time")) history_entries = models.ManyToManyField("history.HistoryEntry", null=True, blank=True, - verbose_name="history entries", + verbose_name=_("history entries"), related_name="+") notify_users = models.ManyToManyField("users.User", null=True, blank=True, - verbose_name="notify users", + verbose_name=_("notify users"), related_name="+") project = models.ForeignKey("projects.Project", null=False, blank=False, - verbose_name="project",related_name="+") + verbose_name=_("project"),related_name="+") history_type = models.SmallIntegerField(choices=HISTORY_TYPE_CHOICES) diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index b066da34..d8bfaa24 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -22,6 +22,7 @@ from django.contrib.contenttypes.models import ContentType from django.utils import timezone from django.db import transaction from django.conf import settings +from django.utils.translation import ugettext as _ from djmail import template_mail @@ -37,6 +38,7 @@ from taiga.users.models import User from .models import HistoryChangeNotification + def notify_policy_exists(project, user) -> bool: """ Check if policy exists for specified project @@ -58,7 +60,7 @@ def create_notify_policy(project, user, level=NotifyLevel.notwatch): user=user, notify_level=level) except IntegrityError as e: - raise exc.IntegrityError("Notify exists for specified user and project") from e + raise exc.IntegrityError(_("Notify exists for specified user and project")) from e def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.notwatch): @@ -72,7 +74,7 @@ def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.notwatch defaults={"notify_level": level}) return result[0] except IntegrityError as e: - raise exc.IntegrityError("Notify exists for specified user and project") from e + raise exc.IntegrityError(_("Notify exists for specified user and project")) from e def get_notify_policy(project, user): @@ -256,8 +258,7 @@ def send_sync_notifications(notification_id): obj, _ = get_last_snapshot_for_key(notification.key) obj_class = get_model_from_key(obj.key) - context = { - "obj_class": obj_class, + context = {"obj_class": obj_class, "snapshot": obj.snapshot, "project": notification.project, "changer": notification.owner, diff --git a/taiga/projects/notifications/validators.py b/taiga/projects/notifications/validators.py index 5088d446..66a07d98 100644 --- a/taiga/projects/notifications/validators.py +++ b/taiga/projects/notifications/validators.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework import serializers @@ -40,6 +40,6 @@ class WatchersValidator: # in project members list result = set(users).difference(set(project.members.all())) if result: - raise serializers.ValidationError("Watchers contains invalid users") + raise serializers.ValidationError(_("Watchers contains invalid users")) return attrs diff --git a/taiga/projects/occ/mixins.py b/taiga/projects/occ/mixins.py index c9bdeeac..0e37685b 100644 --- a/taiga/projects/occ/mixins.py +++ b/taiga/projects/occ/mixins.py @@ -31,7 +31,7 @@ class OCCResourceMixin(object): try: param_version = param_version and int(param_version) except (ValueError, TypeError): - raise exc.WrongArguments({"version": "The version must be an integer"}) + raise exc.WrongArguments({"version": _("The version must be an integer")}) return param_version @@ -52,7 +52,7 @@ class OCCResourceMixin(object): # Extract param version param_version = self._extract_param_version() if not self._validate_param_version(param_version, current_version): - raise exc.WrongArguments({"version": "The version is not valid"}) + raise exc.WrongArguments({"version": _("The version is not valid")}) if current_version != param_version: diff_versions = current_version - param_version @@ -68,7 +68,7 @@ class OCCResourceMixin(object): both_modified = modifying_fields & modified_fields if both_modified: - raise exc.WrongArguments({"version": "The version doesn't match with the current one"}) + raise exc.WrongArguments({"version": _("The version doesn't match with the current one")}) if obj.id: obj.version = models.F('version') + 1 diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 08b900a7..5483e3d6 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from taiga.base.api.permissions import TaigaResourcePermission from taiga.base.api.permissions import HasProjectPerm diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 5d9d3a4d..2dfb27aa 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -15,7 +15,7 @@ # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from django.db.models import Q from rest_framework import serializers @@ -61,7 +61,7 @@ class PointsSerializer(ModelSerializer): qs = models.Points.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 @@ -85,7 +85,7 @@ class UserStoryStatusSerializer(ModelSerializer): 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 @@ -107,7 +107,7 @@ class TaskStatusSerializer(ModelSerializer): qs = models.TaskStatus.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 @@ -139,7 +139,7 @@ class IssueStatusSerializer(ModelSerializer): qs = models.IssueStatus.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 @@ -310,7 +310,7 @@ class ProjectSerializer(ModelSerializer): """ value = attrs[source] if value is None: - raise serializers.ValidationError("Total milestones must be major or equal to zero") + raise serializers.ValidationError(_("Total milestones must be major or equal to zero")) return attrs diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py index c852459f..b7586cb5 100644 --- a/taiga/projects/services/stats.py +++ b/taiga/projects/services/stats.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.utils.translation import ugettext as _ from django.db.models import Q, Count from django.apps import apps import datetime @@ -21,6 +22,7 @@ import copy from taiga.projects.history.models import HistoryEntry + def _get_milestones_stats_for_backlog(project): """ Get collection of stats for each millestone of project. @@ -67,7 +69,7 @@ def _get_milestones_stats_for_backlog(project): current_client_increment += sum(ml.client_increment_points.values()) else: - milestone_name = "Future sprint" + milestone_name = _("Future sprint") team_increment = current_team_increment + future_team_increment, client_increment = current_client_increment + future_client_increment, current_evolution = None @@ -84,7 +86,7 @@ def _get_milestones_stats_for_backlog(project): evolution = (project.total_story_points - current_evolution if current_evolution is not None and project.total_story_points else None) yield { - 'name': 'Project End', + 'name': _('Project End'), 'optimal': optimal_points, 'evolution': evolution, 'team-increment': team_increment, @@ -119,8 +121,8 @@ def _count_owned_object(user_obj, counting_storage): else: counting_storage[0] = {} counting_storage[0]['count'] = 1 - counting_storage[0]['username'] = 'Unassigned' - counting_storage[0]['name'] = 'Unassigned' + counting_storage[0]['username'] = _('Unassigned') + counting_storage[0]['name'] = _('Unassigned') counting_storage[0]['id'] = 0 counting_storage[0]['color'] = 'black' diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 92d94908..64b05d9d 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from taiga.base.api.utils import get_object_or_404 from taiga.base import filters, response diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index c7f1293b..38089fe7 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework import serializers diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 7c31670b..3efd7b8f 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -14,9 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index 700fa3ab..9659261e 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework import serializers diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index f67150bf..d21269df 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework.permissions import IsAuthenticated diff --git a/taiga/users/api.py b/taiga/users/api.py index 36dff9b1..9265865d 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -18,7 +18,7 @@ import uuid from django.apps import apps from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from django.core.validators import validate_email from django.core.exceptions import ValidationError from django.conf import settings diff --git a/taiga/users/services.py b/taiga/users/services.py index 9366cee8..99b8975d 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -21,6 +21,7 @@ This model contains a domain logic for users application. from django.apps import apps from django.db.models import Q from django.conf import settings +from django.utils.translation import ugettext as _ from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.exceptions import InvalidImageFormatError @@ -44,11 +45,11 @@ def get_and_validate_user(*, username:str, password:str) -> bool: qs = user_model.objects.filter(Q(username=username) | Q(email=username)) if len(qs) == 0: - raise exc.WrongArguments("Username or password does not matches user.") + raise exc.WrongArguments(_("Username or password does not matches user.")) user = qs[0] if not user.check_password(password): - raise exc.WrongArguments("Username or password does not matches user.") + raise exc.WrongArguments(_("Username or password does not matches user.")) return user @@ -80,6 +81,10 @@ def get_big_photo_url(photo): def get_big_photo_or_gravatar_url(user): """Get the user's big photo/gravatar url.""" - if user: - return get_big_photo_url(user.photo) if user.photo else get_gravatar_url(user.email, size=settings.DEFAULT_BIG_AVATAR_SIZE) - return "" + if not user: + return "" + + if user.photo: + return get_big_photo_url(user.photo) + else: + return get_gravatar_url(user.email, size=settings.DEFAULT_BIG_AVATAR_SIZE) diff --git a/taiga/users/validators.py b/taiga/users/validators.py index 1f26ffa5..7a83ed3c 100644 --- a/taiga/users/validators.py +++ b/taiga/users/validators.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from rest_framework import serializers diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py index 31324c17..6755f3ac 100644 --- a/taiga/userstorage/api.py +++ b/taiga/userstorage/api.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _ from django.db import IntegrityError from taiga.base.api import ModelCrudViewSet diff --git a/taiga/webhooks/models.py b/taiga/webhooks/models.py index 41f6fc84..0704b77b 100644 --- a/taiga/webhooks/models.py +++ b/taiga/webhooks/models.py @@ -36,12 +36,12 @@ class WebhookLog(models.Model): webhook = models.ForeignKey(Webhook, null=False, blank=False, related_name="logs") url = models.URLField(null=False, blank=False, verbose_name=_("URL")) - status = models.IntegerField(null=False, blank=False, verbose_name=_("Status code")) - request_data = JsonField(null=False, blank=False, verbose_name=_("Request data")) - request_headers = JsonField(null=False, blank=False, verbose_name=_("Request headers"), default={}) - response_data = models.TextField(null=False, blank=False, verbose_name=_("Response data")) - response_headers = JsonField(null=False, blank=False, verbose_name=_("Response headers"), default={}) - duration = models.FloatField(null=False, blank=False, verbose_name=_("Duration"), default=0) + status = models.IntegerField(null=False, blank=False, verbose_name=_("status code")) + request_data = JsonField(null=False, blank=False, verbose_name=_("request data")) + request_headers = JsonField(null=False, blank=False, verbose_name=_("request headers"), default={}) + response_data = models.TextField(null=False, blank=False, verbose_name=_("response data")) + response_headers = JsonField(null=False, blank=False, verbose_name=_("response headers"), default={}) + duration = models.FloatField(null=False, blank=False, verbose_name=_("duration"), default=0) created = models.DateTimeField(auto_now_add=True) class Meta: From 910d71eefc4433952e48a58fa43cfa00a1f3bd60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 7 Apr 2015 23:34:44 +0200 Subject: [PATCH 17/96] Add code of django-restframwork to taiga --- locale/en/LC_MESSAGES/django.po | 2 +- requirements.txt | 2 +- settings/common.py | 2 +- taiga/auth/api.py | 3 +- taiga/auth/backends.py | 4 +- taiga/auth/serializers.py | 5 +- taiga/base/api/__init__.py | 9 + taiga/base/api/authentication.py | 148 +++ taiga/base/api/fields.py | 1048 +++++++++++++++ taiga/base/api/generics.py | 148 +-- taiga/base/api/mixins.py | 2 +- taiga/base/api/negotiation.py | 111 ++ taiga/base/api/pagination.py | 105 +- taiga/base/api/parsers.py | 220 +++ taiga/base/api/relations.py | 628 +++++++++ taiga/base/api/renderers.py | 613 +++++++++ taiga/base/api/request.py | 440 ++++++ taiga/base/api/reverse.py | 41 + taiga/base/api/serializers.py | 1182 +++++++++++++++++ taiga/base/api/settings.py | 226 ++++ .../api/static/api/css/bootstrap-tweaks.css | 206 +++ .../base/api/static/api/css/bootstrap.min.css | 841 ++++++++++++ taiga/base/api/static/api/css/default.css | 91 ++ taiga/base/api/static/api/css/prettify.css | 50 + .../api/img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../static/api/img/glyphicons-halflings.png | Bin 0 -> 12762 bytes taiga/base/api/static/api/img/grid.png | Bin 0 -> 1458 bytes taiga/base/api/static/api/js/bootstrap.min.js | 7 + taiga/base/api/static/api/js/default.js | 78 ++ .../api/static/api/js/jquery-1.8.1-min.js | 2 + taiga/base/api/static/api/js/prettify-min.js | 48 + taiga/base/api/templates/api/api.html | 3 + taiga/base/api/templates/api/base.html | 237 ++++ taiga/base/api/templates/api/form.html | 15 + taiga/base/api/templates/api/login.html | 3 + taiga/base/api/templates/api/login_base.html | 53 + .../base/api/templates/api/raw_data_form.html | 12 + taiga/base/api/templatetags/__init__.py | 0 taiga/base/api/templatetags/api.py | 233 ++++ taiga/base/api/throttling.py | 255 ++++ taiga/base/api/urlpatterns.py | 82 ++ taiga/base/api/urls.py | 43 + taiga/base/api/utils/__init__.py | 33 + taiga/base/api/utils/breadcrumbs.py | 74 ++ taiga/base/api/utils/encoders.py | 81 ++ taiga/base/api/utils/formatting.py | 95 ++ taiga/base/api/utils/mediatypes.py | 107 ++ taiga/base/api/views.py | 21 +- taiga/base/api/viewsets.py | 13 +- taiga/base/apps.py | 9 - taiga/base/exceptions.py | 97 +- taiga/base/fields.py | 77 ++ taiga/base/filters.py | 17 +- taiga/base/monkey.py | 50 - taiga/base/neighbors.py | 24 + taiga/base/response.py | 87 +- taiga/base/routers.py | 6 +- taiga/base/serializers.py | 159 --- taiga/base/status.py | 89 ++ taiga/base/throttling.py | 2 +- taiga/base/utils/json.py | 6 +- taiga/export_import/renderers.py | 2 +- taiga/export_import/serializers.py | 7 +- taiga/feedback/serializers.py | 2 +- taiga/locale/en/LC_MESSAGES/django.po | 352 +++-- taiga/projects/attachments/serializers.py | 3 +- .../projects/custom_attributes/serializers.py | 7 +- taiga/projects/history/serializers.py | 5 +- taiga/projects/issues/serializers.py | 13 +- taiga/projects/milestones/serializers.py | 2 +- taiga/projects/milestones/validators.py | 2 +- taiga/projects/notifications/serializers.py | 2 +- taiga/projects/notifications/validators.py | 3 +- taiga/projects/occ/mixins.py | 1 + taiga/projects/references/serializers.py | 2 +- taiga/projects/serializers.py | 34 +- taiga/projects/tasks/serializers.py | 18 +- taiga/projects/tasks/validators.py | 2 +- taiga/projects/userstories/api.py | 2 +- taiga/projects/userstories/serializers.py | 21 +- taiga/projects/validators.py | 2 +- taiga/projects/votes/serializers.py | 2 +- taiga/projects/wiki/api.py | 2 +- taiga/projects/wiki/serializers.py | 2 +- taiga/searches/api.py | 2 +- taiga/timeline/serializers.py | 4 +- taiga/urls.py | 2 +- taiga/users/serializers.py | 18 +- taiga/users/validators.py | 2 +- taiga/userstorage/serializers.py | 4 +- taiga/webhooks/serializers.py | 5 +- taiga/webhooks/tasks.py | 3 +- 92 files changed, 8186 insertions(+), 587 deletions(-) create mode 100644 taiga/base/api/authentication.py create mode 100644 taiga/base/api/fields.py create mode 100644 taiga/base/api/negotiation.py create mode 100644 taiga/base/api/parsers.py create mode 100644 taiga/base/api/relations.py create mode 100644 taiga/base/api/renderers.py create mode 100644 taiga/base/api/request.py create mode 100644 taiga/base/api/reverse.py create mode 100644 taiga/base/api/serializers.py create mode 100644 taiga/base/api/settings.py create mode 100644 taiga/base/api/static/api/css/bootstrap-tweaks.css create mode 100644 taiga/base/api/static/api/css/bootstrap.min.css create mode 100644 taiga/base/api/static/api/css/default.css create mode 100644 taiga/base/api/static/api/css/prettify.css create mode 100644 taiga/base/api/static/api/img/glyphicons-halflings-white.png create mode 100644 taiga/base/api/static/api/img/glyphicons-halflings.png create mode 100644 taiga/base/api/static/api/img/grid.png create mode 100644 taiga/base/api/static/api/js/bootstrap.min.js create mode 100644 taiga/base/api/static/api/js/default.js create mode 100644 taiga/base/api/static/api/js/jquery-1.8.1-min.js create mode 100644 taiga/base/api/static/api/js/prettify-min.js create mode 100644 taiga/base/api/templates/api/api.html create mode 100644 taiga/base/api/templates/api/base.html create mode 100644 taiga/base/api/templates/api/form.html create mode 100644 taiga/base/api/templates/api/login.html create mode 100644 taiga/base/api/templates/api/login_base.html create mode 100644 taiga/base/api/templates/api/raw_data_form.html create mode 100644 taiga/base/api/templatetags/__init__.py create mode 100644 taiga/base/api/templatetags/api.py create mode 100644 taiga/base/api/throttling.py create mode 100644 taiga/base/api/urlpatterns.py create mode 100644 taiga/base/api/urls.py create mode 100644 taiga/base/api/utils/__init__.py create mode 100644 taiga/base/api/utils/breadcrumbs.py create mode 100644 taiga/base/api/utils/encoders.py create mode 100644 taiga/base/api/utils/formatting.py create mode 100644 taiga/base/api/utils/mediatypes.py create mode 100644 taiga/base/fields.py delete mode 100644 taiga/base/serializers.py create mode 100644 taiga/base/status.py diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 6a22a978..5a11fd04 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-04-06 17:04+0200\n" +"POT-Creation-Date: 2015-04-09 20:07+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" diff --git a/requirements.txt b/requirements.txt index ac564e0d..77a434c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/settings/common.py b/settings/common.py index b75a4e8c..66d29214 100644 --- a/settings/common.py +++ b/settings/common.py @@ -269,6 +269,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "taiga.base", + "taiga.base.api", "taiga.events", "taiga.front", "taiga.users", @@ -295,7 +296,6 @@ INSTALLED_APPS = [ "taiga.hooks.bitbucket", "taiga.webhooks", - "rest_framework", "djmail", "django_jinja", "django_jinja.contrib._humanize", diff --git a/taiga/auth/api.py b/taiga/auth/api.py index 717269ae..a666dc2e 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -20,8 +20,7 @@ from enum import Enum 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 diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py index 7f156c08..fe44544b 100644 --- a/taiga/auth/backends.py +++ b/taiga/auth/backends.py @@ -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. diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py index 23789656..5224b6d3 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/serializers.py @@ -14,11 +14,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/taiga/base/api/__init__.py b/taiga/base/api/__init__.py index 845821b2..69083fa5 100644 --- a/taiga/base/api/__init__.py +++ b/taiga/base/api/__init__.py @@ -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 diff --git a/taiga/base/api/authentication.py b/taiga/base/api/authentication.py new file mode 100644 index 00000000..8343fa30 --- /dev/null +++ b/taiga/base/api/authentication.py @@ -0,0 +1,148 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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) diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py new file mode 100644 index 00000000..48a2250c --- /dev/null +++ b/taiga/base/api/fields.py @@ -0,0 +1,1048 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2014, Tom Christie + +""" +Serializer fields perform validation on incoming data. + +They are very similar to Django's form fields. +""" +from django import forms +from django.conf import settings +from django.core import validators +from django.core.exceptions import ValidationError +from django.db.models.fields import BLANK_CHOICE_DASH +from django.forms import widgets +from django.http import QueryDict +from django.utils import six +from django.utils import timezone +from django.utils.dateparse import parse_date +from django.utils.dateparse import parse_datetime +from django.utils.dateparse import parse_time +from django.utils.encoding import smart_text +from django.utils.encoding import force_text +from django.utils.encoding import is_protected_type +from django.utils.functional import Promise +from django.utils.translation import ugettext_lazy as _ +from django.utils.datastructures import SortedDict + +from . import ISO_8601 +from .settings import api_settings + +import copy +import datetime +import inspect +import re +import warnings +from decimal import Decimal, DecimalException + + +def is_non_str_iterable(obj): + if (isinstance(obj, str) or + (isinstance(obj, Promise) and obj._delegate_text)): + return False + return hasattr(obj, "__iter__") + + +def is_simple_callable(obj): + """ + True if the object is a callable that takes no arguments. + """ + function = inspect.isfunction(obj) + method = inspect.ismethod(obj) + + if not (function or method): + return False + + args, _, _, defaults = inspect.getargspec(obj) + len_args = len(args) if function else len(args) - 1 + len_defaults = len(defaults) if defaults else 0 + return len_args <= len_defaults + + +def get_component(obj, attr_name): + """ + Given an object, and an attribute name, + return that attribute on the object. + """ + if isinstance(obj, dict): + val = obj.get(attr_name) + else: + val = getattr(obj, attr_name) + + if is_simple_callable(val): + return val() + return val + + +def readable_datetime_formats(formats): + format = ", ".join(formats).replace(ISO_8601, + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]") + return humanize_strptime(format) + + +def readable_date_formats(formats): + format = ", ".join(formats).replace(ISO_8601, "YYYY[-MM[-DD]]") + return humanize_strptime(format) + + +def readable_time_formats(formats): + format = ", ".join(formats).replace(ISO_8601, "hh:mm[:ss[.uuuuuu]]") + return humanize_strptime(format) + + +def humanize_strptime(format_string): + # Note that we're missing some of the locale specific mappings that + # don't really make sense. + mapping = { + "%Y": "YYYY", + "%y": "YY", + "%m": "MM", + "%b": "[Jan-Dec]", + "%B": "[January-December]", + "%d": "DD", + "%H": "hh", + "%I": "hh", # Requires '%p' to differentiate from '%H'. + "%M": "mm", + "%S": "ss", + "%f": "uuuuuu", + "%a": "[Mon-Sun]", + "%A": "[Monday-Sunday]", + "%p": "[AM|PM]", + "%z": "[+HHMM|-HHMM]" + } + for key, val in mapping.items(): + format_string = format_string.replace(key, val) + return format_string + + +class Field(object): + read_only = True + creation_counter = 0 + empty = "" + type_name = None + partial = False + use_files = False + form_field_class = forms.CharField + type_label = "field" + widget = None + + def __init__(self, source=None, label=None, help_text=None): + self.parent = None + + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 + + self.source = source + + if label is not None: + self.label = smart_text(label) + else: + self.label = None + + self.help_text = help_text + self._errors = [] + self._value = None + self._name = None + + @property + def errors(self): + return self._errors + + def widget_html(self): + if not self.widget: + return "" + return self.widget.render(self._name, self._value) + + def label_tag(self): + return "" % (self._name, self.label) + + def initialize(self, parent, field_name): + """ + Called to set up a field prior to field_to_native or field_from_native. + + parent - The parent serializer. + model_field - The model field this field corresponds to, if one exists. + """ + self.parent = parent + self.root = parent.root or parent + self.context = self.root.context + self.partial = self.root.partial + if self.partial: + self.required = False + + def field_from_native(self, data, files, field_name, into): + """ + Given a dictionary and a field name, updates the dictionary `into`, + with the field and it's deserialized value. + """ + return + + def field_to_native(self, obj, field_name): + """ + Given and object and a field name, returns the value that should be + serialized for that field. + """ + if obj is None: + return self.empty + + if self.source == "*": + return self.to_native(obj) + + source = self.source or field_name + value = obj + + for component in source.split("."): + value = get_component(value, component) + if value is None: + break + + return self.to_native(value) + + def to_native(self, value): + """ + Converts the field's value into it's simple representation. + """ + if is_simple_callable(value): + value = value() + + if is_protected_type(value): + return value + elif (is_non_str_iterable(value) and + not isinstance(value, (dict, six.string_types))): + return [self.to_native(item) for item in value] + elif isinstance(value, dict): + # Make sure we preserve field ordering, if it exists + ret = SortedDict() + for key, val in value.items(): + ret[key] = self.to_native(val) + return ret + return force_text(value) + + def attributes(self): + """ + Returns a dictionary of attributes to be used when serializing to xml. + """ + if self.type_name: + return {"type": self.type_name} + return {} + + def metadata(self): + metadata = SortedDict() + metadata["type"] = self.type_label + metadata["required"] = getattr(self, "required", False) + optional_attrs = ["read_only", "label", "help_text", + "min_length", "max_length"] + for attr in optional_attrs: + value = getattr(self, attr, None) + if value is not None and value != "": + metadata[attr] = force_text(value, strings_only=True) + return metadata + + +class WritableField(Field): + """ + Base for read/write fields. + """ + write_only = False + default_validators = [] + default_error_messages = { + "required": _("This field is required."), + "invalid": _("Invalid value."), + } + widget = widgets.TextInput + default = None + + def __init__(self, source=None, label=None, help_text=None, + read_only=False, write_only=False, required=None, + validators=[], error_messages=None, widget=None, + default=None, blank=None): + + # "blank" is to be deprecated in favor of "required" + if blank is not None: + warnings.warn("The `blank` keyword argument is deprecated. " + "Use the `required` keyword argument instead.", + DeprecationWarning, stacklevel=2) + required = not(blank) + + super(WritableField, self).__init__(source=source, label=label, help_text=help_text) + + self.read_only = read_only + self.write_only = write_only + + assert not (read_only and write_only), "Cannot set read_only=True and write_only=True" + + if required is None: + self.required = not(read_only) + else: + assert not (read_only and required), "Cannot set required=True and read_only=True" + self.required = required + + messages = {} + for c in reversed(self.__class__.__mro__): + messages.update(getattr(c, "default_error_messages", {})) + messages.update(error_messages or {}) + self.error_messages = messages + + self.validators = self.default_validators + validators + self.default = default if default is not None else self.default + + # Widgets are ony used for HTML forms. + widget = widget or self.widget + if isinstance(widget, type): + widget = widget() + self.widget = widget + + def __deepcopy__(self, memo): + result = copy.copy(self) + memo[id(self)] = result + result.validators = self.validators[:] + return result + + def get_default_value(self): + if is_simple_callable(self.default): + return self.default() + return self.default + + def validate(self, value): + if value in validators.EMPTY_VALUES and self.required: + raise ValidationError(self.error_messages["required"]) + + def run_validators(self, value): + if value in validators.EMPTY_VALUES: + return + errors = [] + for v in self.validators: + try: + v(value) + except ValidationError as e: + if hasattr(e, "code") and e.code in self.error_messages: + message = self.error_messages[e.code] + if e.params: + message = message % e.params + errors.append(message) + else: + errors.extend(e.messages) + if errors: + raise ValidationError(errors) + + def field_to_native(self, obj, field_name): + if self.write_only: + return None + return super(WritableField, self).field_to_native(obj, field_name) + + def field_from_native(self, data, files, field_name, into): + """ + Given a dictionary and a field name, updates the dictionary `into`, + with the field and it's deserialized value. + """ + if self.read_only: + return + + try: + data = data or {} + if self.use_files: + files = files or {} + try: + native = files[field_name] + except KeyError: + native = data[field_name] + else: + native = data[field_name] + except KeyError: + if self.default is not None and not self.partial: + # Note: partial updates shouldn't set defaults + native = self.get_default_value() + else: + if self.required: + raise ValidationError(self.error_messages["required"]) + return + + value = self.from_native(native) + if self.source == "*": + if value: + into.update(value) + else: + self.validate(value) + self.run_validators(value) + into[self.source or field_name] = value + + def from_native(self, value): + """ + Reverts a simple representation back to the field's value. + """ + return value + + +class ModelField(WritableField): + """ + A generic field that can be used against an arbitrary model field. + """ + def __init__(self, *args, **kwargs): + try: + self.model_field = kwargs.pop("model_field") + except KeyError: + raise ValueError("ModelField requires 'model_field' kwarg") + + self.min_length = kwargs.pop("min_length", + getattr(self.model_field, "min_length", None)) + self.max_length = kwargs.pop("max_length", + getattr(self.model_field, "max_length", None)) + self.min_value = kwargs.pop("min_value", + getattr(self.model_field, "min_value", None)) + self.max_value = kwargs.pop("max_value", + getattr(self.model_field, "max_value", None)) + + super(ModelField, self).__init__(*args, **kwargs) + + if self.min_length is not None: + self.validators.append(validators.MinLengthValidator(self.min_length)) + if self.max_length is not None: + self.validators.append(validators.MaxLengthValidator(self.max_length)) + if self.min_value is not None: + self.validators.append(validators.MinValueValidator(self.min_value)) + if self.max_value is not None: + self.validators.append(validators.MaxValueValidator(self.max_value)) + + def from_native(self, value): + rel = getattr(self.model_field, "rel", None) + if rel is not None: + return rel.to._meta.get_field(rel.field_name).to_python(value) + else: + return self.model_field.to_python(value) + + def field_to_native(self, obj, field_name): + value = self.model_field._get_val_from_obj(obj) + if is_protected_type(value): + return value + return self.model_field.value_to_string(obj) + + def attributes(self): + return { + "type": self.model_field.get_internal_type() + } + + +##### Typed Fields ##### + +class BooleanField(WritableField): + type_name = "BooleanField" + type_label = "boolean" + form_field_class = forms.BooleanField + widget = widgets.CheckboxInput + default_error_messages = { + "invalid": _("'%s' value must be either True or False."), + } + empty = False + + def field_from_native(self, data, files, field_name, into): + # HTML checkboxes do not explicitly represent unchecked as `False` + # we deal with that here... + if isinstance(data, QueryDict) and self.default is None: + self.default = False + + return super(BooleanField, self).field_from_native( + data, files, field_name, into + ) + + def from_native(self, value): + if value in ("true", "t", "True", "1"): + return True + if value in ("false", "f", "False", "0"): + return False + return bool(value) + + +class CharField(WritableField): + type_name = "CharField" + type_label = "string" + form_field_class = forms.CharField + + def __init__(self, max_length=None, min_length=None, *args, **kwargs): + self.max_length, self.min_length = max_length, min_length + super(CharField, self).__init__(*args, **kwargs) + if min_length is not None: + self.validators.append(validators.MinLengthValidator(min_length)) + if max_length is not None: + self.validators.append(validators.MaxLengthValidator(max_length)) + + def from_native(self, value): + if isinstance(value, six.string_types) or value is None: + return value + return smart_text(value) + + +class URLField(CharField): + type_name = "URLField" + type_label = "url" + + def __init__(self, **kwargs): + if not "validators" in kwargs: + kwargs["validators"] = [validators.URLValidator()] + super(URLField, self).__init__(**kwargs) + + +class SlugField(CharField): + type_name = "SlugField" + type_label = "slug" + form_field_class = forms.SlugField + + default_error_messages = { + "invalid": _("Enter a valid 'slug' consisting of letters, numbers," + " underscores or hyphens."), + } + default_validators = [validators.validate_slug] + + def __init__(self, *args, **kwargs): + super(SlugField, self).__init__(*args, **kwargs) + + +class ChoiceField(WritableField): + type_name = "ChoiceField" + type_label = "multiple choice" + form_field_class = forms.ChoiceField + widget = widgets.Select + default_error_messages = { + "invalid_choice": _("Select a valid choice. %(value)s is not one of " + "the available choices."), + } + + def __init__(self, choices=(), *args, **kwargs): + self.empty = kwargs.pop("empty", "") + super(ChoiceField, self).__init__(*args, **kwargs) + self.choices = choices + if not self.required: + self.choices = BLANK_CHOICE_DASH + self.choices + + def _get_choices(self): + return self._choices + + 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) + + def metadata(self): + data = super(ChoiceField, self).metadata() + data["choices"] = [{"value": v, "display_name": n} for v, n in self.choices] + return data + + def validate(self, value): + """ + Validates that the input is in self.choices. + """ + super(ChoiceField, self).validate(value) + if value and not self.valid_value(value): + raise ValidationError(self.error_messages["invalid_choice"] % {"value": value}) + + def valid_value(self, value): + """ + Check to see if the provided value is a valid choice. + """ + for k, v in self.choices: + if isinstance(v, (list, tuple)): + # This is an optgroup, so look inside the group for options + for k2, v2 in v: + if value == smart_text(k2): + return True + else: + if value == smart_text(k) or value == k: + return True + return False + + def from_native(self, value): + value = super(ChoiceField, self).from_native(value) + if value == self.empty or value in validators.EMPTY_VALUES: + return self.empty + return value + + +class EmailField(CharField): + type_name = "EmailField" + type_label = "email" + form_field_class = forms.EmailField + + default_error_messages = { + "invalid": _("Enter a valid email address."), + } + default_validators = [validators.validate_email] + + def from_native(self, value): + ret = super(EmailField, self).from_native(value) + if ret is None: + return None + return ret.strip() + + +class RegexField(CharField): + type_name = "RegexField" + type_label = "regex" + form_field_class = forms.RegexField + + def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): + super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) + self.regex = regex + + def _get_regex(self): + return self._regex + + def _set_regex(self, regex): + if isinstance(regex, six.string_types): + regex = re.compile(regex) + self._regex = regex + if hasattr(self, "_regex_validator") and self._regex_validator in self.validators: + self.validators.remove(self._regex_validator) + self._regex_validator = validators.RegexValidator(regex=regex) + self.validators.append(self._regex_validator) + + regex = property(_get_regex, _set_regex) + + +class DateField(WritableField): + type_name = "DateField" + type_label = "date" + widget = widgets.DateInput + form_field_class = forms.DateField + + default_error_messages = { + "invalid": _("Date has wrong format. Use one of these formats instead: %s"), + } + empty = None + input_formats = api_settings.DATE_INPUT_FORMATS + format = api_settings.DATE_FORMAT + + def __init__(self, input_formats=None, format=None, *args, **kwargs): + self.input_formats = input_formats if input_formats is not None else self.input_formats + self.format = format if format is not None else self.format + super(DateField, self).__init__(*args, **kwargs) + + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + + if isinstance(value, datetime.datetime): + if timezone and settings.USE_TZ and timezone.is_aware(value): + # Convert aware datetimes to the default time zone + # before casting them to dates (#17742). + default_timezone = timezone.get_default_timezone() + value = timezone.make_naive(value, default_timezone) + return value.date() + if isinstance(value, datetime.date): + return value + + for format in self.input_formats: + if format.lower() == ISO_8601: + try: + parsed = parse_date(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed + else: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.date() + + msg = self.error_messages["invalid"] % readable_date_formats(self.input_formats) + raise ValidationError(msg) + + def to_native(self, value): + if value is None or self.format is None: + return value + + if isinstance(value, datetime.datetime): + value = value.date() + + if self.format.lower() == ISO_8601: + return value.isoformat() + return value.strftime(self.format) + + +class DateTimeField(WritableField): + type_name = "DateTimeField" + type_label = "datetime" + widget = widgets.DateTimeInput + form_field_class = forms.DateTimeField + + default_error_messages = { + "invalid": _("Datetime has wrong format. Use one of these formats instead: %s"), + } + empty = None + input_formats = api_settings.DATETIME_INPUT_FORMATS + format = api_settings.DATETIME_FORMAT + + def __init__(self, input_formats=None, format=None, *args, **kwargs): + self.input_formats = input_formats if input_formats is not None else self.input_formats + self.format = format if format is not None else self.format + super(DateTimeField, self).__init__(*args, **kwargs) + + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + + if isinstance(value, datetime.datetime): + return value + if isinstance(value, datetime.date): + value = datetime.datetime(value.year, value.month, value.day) + if settings.USE_TZ: + # For backwards compatibility, interpret naive datetimes in + # local time. This won't work during DST change, but we can"t + # do much about it, so we let the exceptions percolate up the + # call stack. + warnings.warn("DateTimeField received a naive datetime (%s)" + " while time zone support is active." % value, + RuntimeWarning) + default_timezone = timezone.get_default_timezone() + value = timezone.make_aware(value, default_timezone) + return value + + for format in self.input_formats: + if format.lower() == ISO_8601: + try: + parsed = parse_datetime(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed + else: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed + + msg = self.error_messages["invalid"] % readable_datetime_formats(self.input_formats) + raise ValidationError(msg) + + def to_native(self, value): + if value is None or self.format is None: + return value + + if self.format.lower() == ISO_8601: + ret = value.isoformat() + if ret.endswith("+00:00"): + ret = ret[:-6] + "Z" + return ret + return value.strftime(self.format) + + +class TimeField(WritableField): + type_name = "TimeField" + type_label = "time" + widget = widgets.TimeInput + form_field_class = forms.TimeField + + default_error_messages = { + "invalid": _("Time has wrong format. Use one of these formats instead: %s"), + } + empty = None + input_formats = api_settings.TIME_INPUT_FORMATS + format = api_settings.TIME_FORMAT + + def __init__(self, input_formats=None, format=None, *args, **kwargs): + self.input_formats = input_formats if input_formats is not None else self.input_formats + self.format = format if format is not None else self.format + super(TimeField, self).__init__(*args, **kwargs) + + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + + if isinstance(value, datetime.time): + return value + + for format in self.input_formats: + if format.lower() == ISO_8601: + try: + parsed = parse_time(value) + except (ValueError, TypeError): + pass + else: + if parsed is not None: + return parsed + else: + try: + parsed = datetime.datetime.strptime(value, format) + except (ValueError, TypeError): + pass + else: + return parsed.time() + + msg = self.error_messages["invalid"] % readable_time_formats(self.input_formats) + raise ValidationError(msg) + + def to_native(self, value): + if value is None or self.format is None: + return value + + if isinstance(value, datetime.datetime): + value = value.time() + + if self.format.lower() == ISO_8601: + return value.isoformat() + return value.strftime(self.format) + + +class IntegerField(WritableField): + type_name = "IntegerField" + type_label = "integer" + form_field_class = forms.IntegerField + empty = 0 + + default_error_messages = { + "invalid": _("Enter a whole number."), + "max_value": _("Ensure this value is less than or equal to %(limit_value)s."), + "min_value": _("Ensure this value is greater than or equal to %(limit_value)s."), + } + + def __init__(self, max_value=None, min_value=None, *args, **kwargs): + self.max_value, self.min_value = max_value, min_value + super(IntegerField, self).__init__(*args, **kwargs) + + if max_value is not None: + self.validators.append(validators.MaxValueValidator(max_value)) + if min_value is not None: + self.validators.append(validators.MinValueValidator(min_value)) + + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + + try: + value = int(str(value)) + except (ValueError, TypeError): + raise ValidationError(self.error_messages["invalid"]) + return value + + +class FloatField(WritableField): + type_name = "FloatField" + type_label = "float" + form_field_class = forms.FloatField + empty = 0 + + default_error_messages = { + "invalid": _('"%s" value must be a float.'), + } + + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + + try: + return float(value) + except (TypeError, ValueError): + msg = self.error_messages["invalid"] % value + raise ValidationError(msg) + + +class DecimalField(WritableField): + type_name = "DecimalField" + type_label = "decimal" + form_field_class = forms.DecimalField + empty = Decimal("0") + + default_error_messages = { + "invalid": _("Enter a number."), + "max_value": _("Ensure this value is less than or equal to %(limit_value)s."), + "min_value": _("Ensure this value is greater than or equal to %(limit_value)s."), + "max_digits": _("Ensure that there are no more than %s digits in total."), + "max_decimal_places": _("Ensure that there are no more than %s decimal places."), + "max_whole_digits": _("Ensure that there are no more than %s digits before the decimal point.") + } + + def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): + self.max_value, self.min_value = max_value, min_value + self.max_digits, self.decimal_places = max_digits, decimal_places + super(DecimalField, self).__init__(*args, **kwargs) + + if max_value is not None: + self.validators.append(validators.MaxValueValidator(max_value)) + if min_value is not None: + self.validators.append(validators.MinValueValidator(min_value)) + + def from_native(self, value): + """ + Validates that the input is a decimal number. Returns a Decimal + instance. Returns None for empty values. Ensures that there are no more + than max_digits in the number, and no more than decimal_places digits + after the decimal point. + """ + if value in validators.EMPTY_VALUES: + return None + value = smart_text(value).strip() + try: + value = Decimal(value) + except DecimalException: + raise ValidationError(self.error_messages["invalid"]) + return value + + def validate(self, value): + super(DecimalField, self).validate(value) + if value in validators.EMPTY_VALUES: + return + # Check for NaN, Inf and -Inf values. We can't compare directly for NaN, + # since it is never equal to itself. However, NaN is the only value that + # isn't equal to itself, so we can use this to identify NaN + if value != value or value == Decimal("Inf") or value == Decimal("-Inf"): + raise ValidationError(self.error_messages["invalid"]) + sign, digittuple, exponent = value.as_tuple() + decimals = abs(exponent) + # digittuple doesn't include any leading zeros. + digits = len(digittuple) + if decimals > digits: + # We have leading zeros up to or past the decimal point. Count + # everything past the decimal point as a digit. We do not count + # 0 before the decimal point as a digit since that would mean + # we would not allow max_digits = decimal_places. + digits = decimals + whole_digits = digits - decimals + + if self.max_digits is not None and digits > self.max_digits: + raise ValidationError(self.error_messages["max_digits"] % self.max_digits) + if self.decimal_places is not None and decimals > self.decimal_places: + raise ValidationError(self.error_messages["max_decimal_places"] % self.decimal_places) + if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places): + raise ValidationError(self.error_messages["max_whole_digits"] % (self.max_digits - self.decimal_places)) + return value + + +class FileField(WritableField): + use_files = True + type_name = "FileField" + type_label = "file upload" + form_field_class = forms.FileField + widget = widgets.FileInput + + default_error_messages = { + "invalid": _("No file was submitted. Check the encoding type on the form."), + "missing": _("No file was submitted."), + "empty": _("The submitted file is empty."), + "max_length": _("Ensure this filename has at most %(max)d characters (it has %(length)d)."), + "contradiction": _("Please either submit a file or check the clear checkbox, not both.") + } + + def __init__(self, *args, **kwargs): + self.max_length = kwargs.pop("max_length", None) + self.allow_empty_file = kwargs.pop("allow_empty_file", False) + super(FileField, self).__init__(*args, **kwargs) + + def from_native(self, data): + if data in validators.EMPTY_VALUES: + return None + + # UploadedFile objects should have name and size attributes. + try: + file_name = data.name + file_size = data.size + except AttributeError: + raise ValidationError(self.error_messages["invalid"]) + + if self.max_length is not None and len(file_name) > self.max_length: + error_values = {"max": self.max_length, "length": len(file_name)} + raise ValidationError(self.error_messages["max_length"] % error_values) + if not file_name: + raise ValidationError(self.error_messages["invalid"]) + if not self.allow_empty_file and not file_size: + raise ValidationError(self.error_messages["empty"]) + + return data + + def to_native(self, value): + return value.name + + +class ImageField(FileField): + use_files = True + type_name = "ImageField" + type_label = "image upload" + form_field_class = forms.ImageField + + default_error_messages = { + "invalid_image": _("Upload a valid image. The file you uploaded was " + "either not an image or a corrupted image."), + } + + def from_native(self, data): + """ + Checks that the file-upload field data contains a valid image (GIF, JPG, + PNG, possibly others -- whatever the Python Imaging Library supports). + """ + f = super(ImageField, self).from_native(data) + if f is None: + return None + + # Try to import PIL in either of the two ways it can end up installed. + try: + from PIL import Image + except ImportError: + try: + import Image + except ImportError: + Image = None + + assert Image is not None, "Either Pillow or PIL must be installed for ImageField support." + + # We need to get a file object for PIL. We might have a path or we might + # have to read the data into memory. + if hasattr(data, "temporary_file_path"): + file = data.temporary_file_path() + else: + if hasattr(data, "read"): + file = six.BytesIO(data.read()) + else: + file = six.BytesIO(data["content"]) + + try: + # load() could spot a truncated JPEG, but it loads the entire + # image in memory, which is a DoS vector. See #3848 and #18520. + # verify() must be called immediately after the constructor. + Image.open(file).verify() + except ImportError: + # Under PyPy, it is possible to import PIL. However, the underlying + # _imaging C module isn't available, so an ImportError will be + # raised. Catch and re-raise. + raise + except Exception: # Python Imaging Library doesn't recognize it as an image + raise ValidationError(self.error_messages["invalid_image"]) + if hasattr(f, "seek") and callable(f.seek): + f.seek(0) + return f + + +class SerializerMethodField(Field): + """ + A field that gets its value by calling a method on the serializer it's attached to. + """ + + def __init__(self, method_name): + self.method_name = method_name + super(SerializerMethodField, self).__init__() + + def field_to_native(self, obj, field_name): + value = getattr(self.parent, self.method_name)(obj) + return self.to_native(value) diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index 9536cb04..feeebf77 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -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,68 +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): """ @@ -189,10 +104,10 @@ class GenericAPIView(views.APIView): """ filter_backends = self.filter_backends or [] if not filter_backends and hasattr(self, 'filter_backend'): - raise RuntimeError(_('The `filter_backend` attribute and `FILTER_BACKEND` setting ' - 'are due to be deprecated in favor of a `filter_backends` ' - 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' - 'a *list* of filter backend classes.')) + raise RuntimeError('The `filter_backend` attribute and `FILTER_BACKEND` setting ' + 'are due to be deprecated in favor of a `filter_backends` ' + 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' + 'a *list* of filter backend classes.') return filter_backends ########################################################### @@ -200,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 @@ -231,9 +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: @@ -257,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): """ @@ -285,16 +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 diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index d28c8d06..f1df9fb1 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -25,8 +25,8 @@ 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 diff --git a/taiga/base/api/negotiation.py b/taiga/base/api/negotiation.py new file mode 100644 index 00000000..60278752 --- /dev/null +++ b/taiga/base/api/negotiation.py @@ -0,0 +1,111 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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(",")] diff --git a/taiga/base/api/pagination.py b/taiga/base/api/pagination.py index 61e6a78b..028d8106 100644 --- a/taiga/base/api/pagination.py +++ b/taiga/base/api/pagination.py @@ -14,19 +14,112 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/taiga/base/api/parsers.py b/taiga/base/api/parsers.py new file mode 100644 index 00000000..1465f601 --- /dev/null +++ b/taiga/base/api/parsers.py @@ -0,0 +1,220 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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 diff --git a/taiga/base/api/relations.py b/taiga/base/api/relations.py new file mode 100644 index 00000000..87fbfb4c --- /dev/null +++ b/taiga/base/api/relations.py @@ -0,0 +1,628 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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() diff --git a/taiga/base/api/renderers.py b/taiga/base/api/renderers.py new file mode 100644 index 00000000..1e2a1cdd --- /dev/null +++ b/taiga/base/api/renderers.py @@ -0,0 +1,613 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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 = "example" + 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) + diff --git a/taiga/base/api/request.py b/taiga/base/api/request.py new file mode 100644 index 00000000..c57d29fc --- /dev/null +++ b/taiga/base/api/request.py @@ -0,0 +1,440 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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) diff --git a/taiga/base/api/reverse.py b/taiga/base/api/reverse.py new file mode 100644 index 00000000..71c7c047 --- /dev/null +++ b/taiga/base/api/reverse.py @@ -0,0 +1,41 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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) diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py new file mode 100644 index 00000000..f82dcd2c --- /dev/null +++ b/taiga/base/api/serializers.py @@ -0,0 +1,1182 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# This code is partially taken from django-rest-framework: +# Copyright (c) 2011-2015, Tom Christie + + +""" +Serializers and ModelSerializers are similar to Forms and ModelForms. +Unlike forms, they are not constrained to dealing with HTML output, and +form encoded input. + +Serialization in REST framework is a two-phase process: + +1. Serializers marshal between complex types like model instances, and +python primitives. +2. The process of marshalling between python primitives and request and +response content is handled by parsers and renderers. +""" +from decimal import Decimal +from django.core.paginator import Page +from django.db import models +from django.forms import widgets +from django.utils import six +from django.utils.datastructures import SortedDict +from django.utils.translation import ugettext as _ + +from .settings import api_settings + +import copy +import datetime +import inspect +import types + +# Note: We do the following so that users of the framework can use this style: +# +# example_field = serializers.CharField(...) +# +# This helps keep the separation between model fields, form fields, and +# serializer fields more explicit. + +from .relations import * +from .fields import * + + +def _resolve_model(obj): + """ + Resolve supplied `obj` to a Django model class. + + `obj` must be a Django model class itself, or a string + representation of one. Useful in situtations like GH #1225 where + Django may not have resolved a string-based reference to a model in + another model's foreign key definition. + + String representations should have the format: + 'appname.ModelName' + """ + if type(obj) == str and len(obj.split(".")) == 2: + app_name, model_name = obj.split(".") + return models.get_model(app_name, model_name) + elif inspect.isclass(obj) and issubclass(obj, models.Model): + return obj + else: + raise ValueError(_("{0} is not a Django model".format(obj))) + + +def pretty_name(name): + """Converts 'first_name' to 'First name'""" + if not name: + return "" + return name.replace("_", " ").capitalize() + + +class RelationsList(list): + _deleted = [] + + +class NestedValidationError(ValidationError): + """ + The default ValidationError behavior is to stringify each item in the list + if the messages are a list of error messages. + + In the case of nested serializers, where the parent has many children, + then the child's `serializer.errors` will be a list of dicts. In the case + of a single child, the `serializer.errors` will be a dict. + + We need to override the default behavior to get properly nested error dicts. + """ + + def __init__(self, message): + if isinstance(message, dict): + self._messages = [message] + else: + self._messages = message + + @property + def messages(self): + return self._messages + + +class DictWithMetadata(dict): + """ + A dict-like object, that can have additional properties attached. + """ + def __getstate__(self): + """ + Used by pickle (e.g., caching). + Overridden to remove the metadata from the dict, since it shouldn't be + pickled and may in some instances be unpickleable. + """ + return dict(self) + + +class SortedDictWithMetadata(SortedDict): + """ + A sorted dict-like object, that can have additional properties attached. + """ + def __getstate__(self): + """ + Used by pickle (e.g., caching). + Overriden to remove the metadata from the dict, since it shouldn't be + pickle and may in some instances be unpickleable. + """ + return SortedDict(self).__dict__ + + +def _is_protected_type(obj): + """ + True if the object is a native datatype that does not need to + be serialized further. + """ + return isinstance(obj, ( + types.NoneType, + int, long, + datetime.datetime, datetime.date, datetime.time, + float, Decimal, + basestring) + ) + + +def _get_declared_fields(bases, attrs): + """ + Create a list of serializer field instances from the passed in "attrs", + plus any fields on the base classes (in "bases"). + + Note that all fields from the base classes are used. + """ + fields = [(field_name, attrs.pop(field_name)) + for field_name, obj in list(six.iteritems(attrs)) + if isinstance(obj, Field)] + fields.sort(key=lambda x: x[1].creation_counter) + + # If this class is subclassing another Serializer, add that Serializer's + # fields. Note that we loop over the bases in *reverse*. This is necessary + # in order to maintain the correct order of fields. + for base in bases[::-1]: + if hasattr(base, "base_fields"): + fields = list(base.base_fields.items()) + fields + + return SortedDict(fields) + + +class SerializerMetaclass(type): + def __new__(cls, name, bases, attrs): + attrs["base_fields"] = _get_declared_fields(bases, attrs) + return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) + + +class SerializerOptions(object): + """ + Meta class options for Serializer + """ + def __init__(self, meta): + self.depth = getattr(meta, "depth", 0) + self.fields = getattr(meta, "fields", ()) + self.exclude = getattr(meta, "exclude", ()) + + +class BaseSerializer(WritableField): + """ + This is the Serializer implementation. + We need to implement it as `BaseSerializer` due to metaclass magicks. + """ + class Meta(object): + pass + + _options_class = SerializerOptions + _dict_class = SortedDictWithMetadata + + def __init__(self, instance=None, data=None, files=None, + context=None, partial=False, many=None, + allow_add_remove=False, **kwargs): + super(BaseSerializer, self).__init__(**kwargs) + self.opts = self._options_class(self.Meta) + self.parent = None + self.root = None + self.partial = partial + self.many = many + self.allow_add_remove = allow_add_remove + + self.context = context or {} + + self.init_data = data + self.init_files = files + self.object = instance + self.fields = self.get_fields() + + self._data = None + self._files = None + self._errors = None + + if many and instance is not None and not hasattr(instance, "__iter__"): + raise ValueError(_("instance should be a queryset or other iterable with many=True")) + + if allow_add_remove and not many: + raise ValueError(_("allow_add_remove should only be used for bulk updates, but you have not set many=True")) + + ##### + # Methods to determine which fields to use when (de)serializing objects. + + def get_default_fields(self): + """ + Return the complete set of default fields for the object, as a dict. + """ + return {} + + def get_fields(self): + """ + Returns the complete set of fields for the object as a dict. + + This will be the set of any explicitly declared fields, + plus the set of fields returned by get_default_fields(). + """ + ret = SortedDict() + + # Get the explicitly declared fields + base_fields = copy.deepcopy(self.base_fields) + for key, field in base_fields.items(): + ret[key] = field + + # Add in the default fields + default_fields = self.get_default_fields() + for key, val in default_fields.items(): + if key not in ret: + ret[key] = val + + # If "fields" is specified, use those fields, in that order. + if self.opts.fields: + assert isinstance(self.opts.fields, (list, tuple)), "`fields` must be a list or tuple" + new = SortedDict() + for key in self.opts.fields: + new[key] = ret[key] + ret = new + + # Remove anything in "exclude" + if self.opts.exclude: + assert isinstance(self.opts.exclude, (list, tuple)), "`exclude` must be a list or tuple" + for key in self.opts.exclude: + ret.pop(key, None) + + for key, field in ret.items(): + field.initialize(parent=self, field_name=key) + + return ret + + ##### + # Methods to convert or revert from objects <--> primitive representations. + + def get_field_key(self, field_name): + """ + Return the key that should be used for a given field. + """ + return field_name + + def restore_fields(self, data, files): + """ + Core of deserialization, together with `restore_object`. + Converts a dictionary of data into a dictionary of deserialized fields. + """ + reverted_data = {} + + if data is not None and not isinstance(data, dict): + self._errors["non_field_errors"] = [_("Invalid data")] + return None + + for field_name, field in self.fields.items(): + field.initialize(parent=self, field_name=field_name) + try: + field.field_from_native(data, files, field_name, reverted_data) + except ValidationError as err: + self._errors[field_name] = list(err.messages) + + return reverted_data + + def perform_validation(self, attrs): + """ + Run `validate_()` 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.partial and source not in attrs: + continue + try: + validate_method = getattr(self, "validate_%s" % field_name, None) + if validate_method: + attrs = validate_method(attrs, source) + except 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 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 + + def validate(self, attrs): + """ + Stub method, to be overridden in Serializer subclasses + """ + return attrs + + def restore_object(self, attrs, instance=None): + """ + Deserialize a dictionary of attributes into an object instance. + You should override this method to control how deserialized objects + are instantiated. + """ + if instance is not None: + instance.update(attrs) + return instance + return attrs + + 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 + + def from_native(self, data, files=None): + """ + Deserialize primitives -> objects. + """ + self._errors = {} + + if data is not None or files is not None: + attrs = self.restore_fields(data, files) + if attrs is not None: + attrs = self.perform_validation(attrs) + else: + self._errors["non_field_errors"] = [_("No input provided")] + + if not self._errors: + return self.restore_object(attrs, instance=getattr(self, "object", None)) + + def augment_field(self, field, field_name, key, value): + # This horrible stuff is to manage serializers rendering to HTML + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = self.init_data.get(key) if self._errors and self.init_data else value + if not field.label: + field.label = pretty_name(key) + return field + + def field_to_native(self, obj, field_name): + """ + Override default so that the serializer can be used as a nested field + across relationships. + """ + if self.write_only: + return None + + if self.source == "*": + return self.to_native(obj) + + # Get the raw field value + try: + 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 is_simple_callable(getattr(value, "all", None)): + return [self.to_native(item) for item in value.all()] + + if value is None: + return None + + if self.many is not None: + many = self.many + else: + many = hasattr(value, "__iter__") and not isinstance(value, (Page, dict, six.text_type)) + + if many: + return [self.to_native(item) for item in value] + return self.to_native(value) + + def field_from_native(self, data, files, field_name, into): + """ + Override default so that the serializer can be used as a writable + nested field across relationships. + """ + if self.read_only: + return + + try: + value = data[field_name] + except KeyError: + if self.default is not None and not self.partial: + # Note: partial updates shouldn't set defaults + value = copy.deepcopy(self.default) + else: + if self.required: + raise ValidationError(self.error_messages["required"]) + return + + # Set the serializer object if it exists + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None + + # If we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, "__iter__") and + is_simple_callable(getattr(obj, "all", None))): + obj = obj.all() + + if self.source == "*": + if value: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) + else: + if value in (None, ""): + into[(self.source or field_name)] = None + else: + kwargs = { + "instance": obj, + "data": value, + "context": self.context, + "partial": self.partial, + "many": self.many, + "allow_add_remove": self.allow_add_remove + } + serializer = self.__class__(**kwargs) + + if serializer.is_valid(): + into[self.source or field_name] = serializer.object + else: + # Propagate errors up to our parent + raise NestedValidationError(serializer.errors) + + def get_identity(self, data): + """ + This hook is required for bulk update. + It is used to determine the canonical identity of a given object. + + Note that the data has not been validated at this point, so we need + to make sure that we catch any cases of incorrect datatypes being + passed to this method. + """ + try: + return data.get("id", None) + except AttributeError: + return None + + @property + def errors(self): + """ + Run deserialization and return error data, + setting self.object if no errors occurred. + """ + if self._errors is None: + data, files = self.init_data, self.init_files + + if self.many is not None: + many = self.many + else: + many = hasattr(data, "__iter__") and not isinstance(data, (Page, dict, six.text_type)) + if many: + warnings.warn("Implicit list/queryset serialization is deprecated. " + "Use the `many=True` flag when instantiating the serializer.", + DeprecationWarning, stacklevel=3) + + if many: + ret = RelationsList() + errors = [] + update = self.object is not None + + if update: + # If this is a bulk update we need to map all the objects + # to a canonical identity so we can determine which + # individual object is being updated for each item in the + # incoming data + objects = self.object + identities = [self.get_identity(self.to_native(obj)) for obj in objects] + identity_to_objects = dict(zip(identities, objects)) + + if hasattr(data, "__iter__") and not isinstance(data, (dict, six.text_type)): + for item in data: + if update: + # Determine which object we"re updating + identity = self.get_identity(item) + self.object = identity_to_objects.pop(identity, None) + if self.object is None and not self.allow_add_remove: + ret.append(None) + errors.append({"non_field_errors": [_("Cannot create a new item, only existing items may be updated.")]}) + continue + + ret.append(self.from_native(item, None)) + errors.append(self._errors) + + if update and self.allow_add_remove: + ret._deleted = identity_to_objects.values() + + self._errors = any(errors) and errors or [] + else: + self._errors = {"non_field_errors": [_("Expected a list of items.")]} + else: + ret = self.from_native(data, files) + + if not self._errors: + self.object = ret + + return self._errors + + def is_valid(self): + return not self.errors + + @property + def data(self): + """ + Returns the serialized data on the serializer. + """ + if self._data is None: + obj = self.object + + if self.many is not None: + many = self.many + else: + many = hasattr(obj, "__iter__") and not isinstance(obj, (Page, dict)) + if many: + warnings.warn("Implicit list/queryset serialization is deprecated. " + "Use the `many=True` flag when instantiating the serializer.", + DeprecationWarning, stacklevel=2) + + if many: + self._data = [self.to_native(item) for item in obj] + else: + self._data = self.to_native(obj) + + return self._data + + def save_object(self, obj, **kwargs): + obj.save(**kwargs) + + def delete_object(self, obj): + obj.delete() + + def save(self, **kwargs): + """ + Save the deserialized object and return it. + """ + # Clear cached _data, which may be invalidated by `save()` + self._data = None + + if isinstance(self.object, list): + [self.save_object(item, **kwargs) for item in self.object] + + if self.object._deleted: + [self.delete_object(item) for item in self.object._deleted] + else: + self.save_object(self.object, **kwargs) + + return self.object + + def metadata(self): + """ + Return a dictionary of metadata about the fields on the serializer. + Useful for things like responding to OPTIONS requests, or generating + API schemas for auto-documentation. + """ + return SortedDict( + [(field_name, field.metadata()) + for field_name, field in six.iteritems(self.fields)] + ) + + +class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): + 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_()` 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 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 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 ModelSerializerOptions(SerializerOptions): + """ + Meta class options for ModelSerializer + """ + def __init__(self, meta): + super(ModelSerializerOptions, self).__init__(meta) + self.model = getattr(meta, "model", None) + self.read_only_fields = getattr(meta, "read_only_fields", ()) + self.write_only_fields = getattr(meta, "write_only_fields", ()) + + +class ModelSerializer((six.with_metaclass(SerializerMetaclass, BaseSerializer))): + """ + A serializer that deals with model instances and querysets. + """ + _options_class = ModelSerializerOptions + + field_mapping = { + models.AutoField: IntegerField, + models.FloatField: FloatField, + models.IntegerField: IntegerField, + models.PositiveIntegerField: IntegerField, + models.SmallIntegerField: IntegerField, + models.PositiveSmallIntegerField: IntegerField, + models.DateTimeField: DateTimeField, + models.DateField: DateField, + models.TimeField: TimeField, + models.DecimalField: DecimalField, + models.EmailField: EmailField, + models.CharField: CharField, + models.URLField: URLField, + models.SlugField: SlugField, + models.TextField: CharField, + models.CommaSeparatedIntegerField: CharField, + models.BooleanField: BooleanField, + models.NullBooleanField: BooleanField, + models.FileField: FileField, + models.ImageField: ImageField, + } + + def get_default_fields(self): + """ + Return all the fields that should be serialized for the model. + """ + + cls = self.opts.model + assert cls is not None, \ + "Serializer class '%s' is missing `model` Meta option" % self.__class__.__name__ + opts = cls._meta.concrete_model._meta + ret = SortedDict() + nested = bool(self.opts.depth) + + # Deal with adding the primary key field + pk_field = opts.pk + while pk_field.rel and pk_field.rel.parent_link: + # If model is a child via multitable inheritance, use parent's pk + pk_field = pk_field.rel.to._meta.pk + + field = self.get_pk_field(pk_field) + if field: + ret[pk_field.name] = field + + # Deal with forward relationships + forward_rels = [field for field in opts.fields if field.serialize] + forward_rels += [field for field in opts.many_to_many if field.serialize] + + for model_field in forward_rels: + has_through_model = False + + if model_field.rel: + to_many = isinstance(model_field, + models.fields.related.ManyToManyField) + related_model = _resolve_model(model_field.rel.to) + + if to_many and not model_field.rel.through._meta.auto_created: + has_through_model = True + + if model_field.rel and nested: + if len(inspect.getargspec(self.get_nested_field).args) == 2: + warnings.warn( + "The `get_nested_field(model_field)` call signature " + "is due to be deprecated. " + "Use `get_nested_field(model_field, related_model, " + "to_many) instead", + PendingDeprecationWarning + ) + field = self.get_nested_field(model_field) + else: + field = self.get_nested_field(model_field, related_model, to_many) + elif model_field.rel: + if len(inspect.getargspec(self.get_nested_field).args) == 3: + warnings.warn( + "The `get_related_field(model_field, to_many)` call " + "signature is due to be deprecated. " + "Use `get_related_field(model_field, related_model, " + "to_many) instead", + PendingDeprecationWarning + ) + field = self.get_related_field(model_field, to_many=to_many) + else: + field = self.get_related_field(model_field, related_model, to_many) + else: + field = self.get_field(model_field) + + if field: + if has_through_model: + field.read_only = True + + ret[model_field.name] = field + + # Deal with reverse relationships + if not self.opts.fields: + reverse_rels = [] + else: + # Reverse relationships are only included if they are explicitly + # present in the `fields` option on the serializer + reverse_rels = opts.get_all_related_objects() + reverse_rels += opts.get_all_related_many_to_many_objects() + + for relation in reverse_rels: + accessor_name = relation.get_accessor_name() + if not self.opts.fields or accessor_name not in self.opts.fields: + continue + related_model = relation.model + to_many = relation.field.rel.multiple + has_through_model = False + is_m2m = isinstance(relation.field, + models.fields.related.ManyToManyField) + + if (is_m2m and + hasattr(relation.field.rel, "through") and + not relation.field.rel.through._meta.auto_created): + has_through_model = True + + if nested: + field = self.get_nested_field(None, related_model, to_many) + else: + field = self.get_related_field(None, related_model, to_many) + + if field: + if has_through_model: + field.read_only = True + + ret[accessor_name] = field + + # Add the `read_only` flag to any fields that have been specified + # in the `read_only_fields` option + for field_name in self.opts.read_only_fields: + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`read_only_fields`, but also added " + "as an explicit field. Remove it from `read_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `read_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) + ret[field_name].read_only = True + + for field_name in self.opts.write_only_fields: + assert field_name not in self.base_fields.keys(), ( + "field '%s' on serializer '%s' specified in " + "`write_only_fields`, but also added " + "as an explicit field. Remove it from `write_only_fields`." % + (field_name, self.__class__.__name__)) + assert field_name in ret, ( + "Non-existant field '%s' specified in `write_only_fields` " + "on serializer '%s'." % + (field_name, self.__class__.__name__)) + ret[field_name].write_only = True + + return ret + + def get_pk_field(self, model_field): + """ + Returns a default instance of the pk field. + """ + return self.get_field(model_field) + + def get_nested_field(self, model_field, related_model, to_many): + """ + Creates a default instance of a nested relational field. + + Note that model_field will be `None` for reverse relationships. + """ + class NestedModelSerializer(ModelSerializer): + class Meta: + model = related_model + depth = self.opts.depth - 1 + + return NestedModelSerializer(many=to_many) + + def get_related_field(self, model_field, related_model, to_many): + """ + Creates a default instance of a flat relational field. + + Note that model_field will be `None` for reverse relationships. + """ + # TODO: filter queryset using: + # .using(db).complex_filter(self.rel.limit_choices_to) + + kwargs = { + "queryset": related_model._default_manager, + "many": to_many + } + + if model_field: + kwargs["required"] = not(model_field.null or model_field.blank) + + return PrimaryKeyRelatedField(**kwargs) + + def get_field(self, model_field): + """ + Creates a default instance of a basic non-relational field. + """ + kwargs = {} + + if model_field.null or model_field.blank: + kwargs["required"] = False + + if isinstance(model_field, models.AutoField) or not model_field.editable: + kwargs["read_only"] = True + + if model_field.has_default(): + kwargs["default"] = model_field.get_default() + + if issubclass(model_field.__class__, models.TextField): + kwargs["widget"] = widgets.Textarea + + if model_field.verbose_name is not None: + kwargs["label"] = model_field.verbose_name + + if model_field.help_text is not None: + kwargs["help_text"] = model_field.help_text + + # TODO: TypedChoiceField? + if model_field.flatchoices: # This ModelField contains choices + kwargs["choices"] = model_field.flatchoices + if model_field.null: + kwargs["empty"] = None + return ChoiceField(**kwargs) + + # put this below the ChoiceField because min_value isn't a valid initializer + if issubclass(model_field.__class__, models.PositiveIntegerField) or\ + issubclass(model_field.__class__, models.PositiveSmallIntegerField): + kwargs["min_value"] = 0 + + attribute_dict = { + models.CharField: ["max_length"], + models.CommaSeparatedIntegerField: ["max_length"], + models.DecimalField: ["max_digits", "decimal_places"], + models.EmailField: ["max_length"], + models.FileField: ["max_length"], + models.ImageField: ["max_length"], + models.SlugField: ["max_length"], + models.URLField: ["max_length"], + } + + if model_field.__class__ in attribute_dict: + attributes = attribute_dict[model_field.__class__] + for attribute in attributes: + kwargs.update({attribute: getattr(model_field, attribute)}) + + try: + return self.field_mapping[model_field.__class__](**kwargs) + except KeyError: + return ModelField(model_field=model_field, **kwargs) + + 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 get_validation_exclusions(self): + """ + Return a list of field names to exclude from model validation. + """ + cls = self.opts.model + opts = cls._meta.concrete_model._meta + exclusions = [field.name for field in opts.fields + opts.many_to_many] + + for field_name, field in self.fields.items(): + field_name = field.source or field_name + if field_name in exclusions \ + and not field.read_only \ + and field.required \ + and not isinstance(field, Serializer): + exclusions.remove(field_name) + return exclusions + + def full_clean(self, instance): + """ + Perform Django's full_clean, and populate the `errors` dictionary + if any validation errors occur. + + Note that we don't perform this inside the `.restore_object()` method, + so that subclasses can override `.restore_object()`, and still get + the full_clean validation checking. + """ + try: + instance.full_clean(exclude=self.get_validation_exclusions()) + except ValidationError as err: + self._errors = err.message_dict + return None + return instance + + def restore_object(self, attrs, instance=None): + """ + Restore the model instance. + """ + m2m_data = {} + related_data = {} + nested_forward_relations = {} + meta = self.opts.model._meta + + # Reverse fk or one-to-one relations + for (obj, model) in meta.get_all_related_objects_with_model(): + field_name = obj.get_accessor_name() + if field_name in attrs: + related_data[field_name] = attrs.pop(field_name) + + # Reverse m2m relations + for (obj, model) in meta.get_all_related_m2m_objects_with_model(): + field_name = obj.get_accessor_name() + if field_name in attrs: + m2m_data[field_name] = attrs.pop(field_name) + + # Forward m2m relations + for field in meta.many_to_many + meta.virtual_fields: + if field.name in attrs: + m2m_data[field.name] = attrs.pop(field.name) + + # Nested forward relations - These need to be marked so we can save + # them before saving the parent model instance. + for field_name in attrs.keys(): + if isinstance(self.fields.get(field_name, None), Serializer): + nested_forward_relations[field_name] = attrs[field_name] + + # Update an existing instance... + if instance is not None: + for key, val in attrs.items(): + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages["required"] + + # ...or create a new instance + else: + instance = self.opts.model(**attrs) + + # Any relations that cannot be set until we"ve + # saved the model get hidden away on these + # private attributes, so we can deal with them + # at the point of save. + instance._related_data = related_data + instance._m2m_data = m2m_data + instance._nested_forward_relations = nested_forward_relations + + return instance + + def from_native(self, data, files): + """ + Override the default method to also include model field validation. + """ + instance = super(ModelSerializer, self).from_native(data, files) + if not self._errors: + return self.full_clean(instance) + + 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().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 + + def save_object(self, obj, **kwargs): + """ + Save the deserialized object. + """ + if getattr(obj, "_nested_forward_relations", None): + # Nested relationships need to be saved before we can save the + # parent instance. + for field_name, sub_object in obj._nested_forward_relations.items(): + if sub_object: + self.save_object(sub_object) + setattr(obj, field_name, sub_object) + + obj.save(**kwargs) + + if getattr(obj, "_m2m_data", None): + for accessor_name, object_list in obj._m2m_data.items(): + setattr(obj, accessor_name, object_list) + del(obj._m2m_data) + + if getattr(obj, "_related_data", None): + related_fields = dict([ + (field.get_accessor_name(), field) + for field, model + in obj._meta.get_all_related_objects_with_model() + ]) + for accessor_name, related in obj._related_data.items(): + if isinstance(related, RelationsList): + # Nested reverse fk relationship + for related_item in related: + fk_field = related_fields[accessor_name].field.name + setattr(related_item, fk_field, obj) + self.save_object(related_item) + + # Delete any removed objects + if related._deleted: + [self.delete_object(item) for item in related._deleted] + + elif isinstance(related, models.Model): + # Nested reverse one-one relationship + fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + setattr(related, fk_field, obj) + self.save_object(related) + else: + # Reverse FK or reverse one-one + setattr(obj, accessor_name, related) + del(obj._related_data) + + +class HyperlinkedModelSerializerOptions(ModelSerializerOptions): + """ + Options for HyperlinkedModelSerializer + """ + def __init__(self, meta): + super(HyperlinkedModelSerializerOptions, self).__init__(meta) + self.view_name = getattr(meta, "view_name", None) + self.lookup_field = getattr(meta, "lookup_field", None) + self.url_field_name = getattr(meta, "url_field_name", api_settings.URL_FIELD_NAME) + + +class HyperlinkedModelSerializer(ModelSerializer): + """ + A subclass of ModelSerializer that uses hyperlinked relationships, + instead of primary key relationships. + """ + _options_class = HyperlinkedModelSerializerOptions + _default_view_name = "%(model_name)s-detail" + _hyperlink_field_class = HyperlinkedRelatedField + _hyperlink_identify_field_class = HyperlinkedIdentityField + + def get_default_fields(self): + fields = super(HyperlinkedModelSerializer, self).get_default_fields() + + if self.opts.view_name is None: + self.opts.view_name = self._get_default_view_name(self.opts.model) + + if self.opts.url_field_name not in fields: + url_field = self._hyperlink_identify_field_class( + view_name=self.opts.view_name, + lookup_field=self.opts.lookup_field + ) + ret = self._dict_class() + ret[self.opts.url_field_name] = url_field + ret.update(fields) + fields = ret + + return fields + + def get_pk_field(self, model_field): + if self.opts.fields and model_field.name in self.opts.fields: + return self.get_field(model_field) + + def get_related_field(self, model_field, related_model, to_many): + """ + Creates a default instance of a flat relational field. + """ + # TODO: filter queryset using: + # .using(db).complex_filter(self.rel.limit_choices_to) + kwargs = { + "queryset": related_model._default_manager, + "view_name": self._get_default_view_name(related_model), + "many": to_many + } + + if model_field: + kwargs["required"] = not(model_field.null or model_field.blank) + + if self.opts.lookup_field: + kwargs["lookup_field"] = self.opts.lookup_field + + return self._hyperlink_field_class(**kwargs) + + def get_identity(self, data): + """ + This hook is required for bulk update. + We need to override the default, to use the url as the identity. + """ + try: + return data.get(self.opts.url_field_name, None) + except AttributeError: + return None + + def _get_default_view_name(self, model): + """ + Return the view name to use if `view_name` is not specified in `Meta` + """ + model_meta = model._meta + format_kwargs = { + "app_label": model_meta.app_label, + "model_name": model_meta.object_name.lower() + } + return self._default_view_name % format_kwargs diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py new file mode 100644 index 00000000..69440ec0 --- /dev/null +++ b/taiga/base/api/settings.py @@ -0,0 +1,226 @@ +# Copyright (C) 2015 Andrey Antukh +# Copyright (C) 2015 Jesús Espino +# Copyright (C) 2015 David Barragán +# 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 . + +# 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) diff --git a/taiga/base/api/static/api/css/bootstrap-tweaks.css b/taiga/base/api/static/api/css/bootstrap-tweaks.css new file mode 100644 index 00000000..b1cd265b --- /dev/null +++ b/taiga/base/api/static/api/css/bootstrap-tweaks.css @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2015 Andrey Antukh + * Copyright (C) 2015 Jesús Espino + * Copyright (C) 2015 David Barragán + * 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 . + * + * 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; +} diff --git a/taiga/base/api/static/api/css/bootstrap.min.css b/taiga/base/api/static/api/css/bootstrap.min.css new file mode 100644 index 00000000..373f4b43 --- /dev/null +++ b/taiga/base/api/static/api/css/bootstrap.min.css @@ -0,0 +1,841 @@ +/*! + * Bootstrap v2.1.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;} +.clearfix:after{clear:both;} +.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;} +.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} +article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} +audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} +audio:not([controls]){display:none;} +html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} +a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +a:hover,a:active{outline:0;} +sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;} +sup{top:-0.5em;} +sub{bottom:-0.25em;} +img{max-width:100%;width:auto\9;height:auto;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic;} +#map_canvas img{max-width:none;} +button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;} +button,input{*overflow:visible;line-height:normal;} +button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;} +button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} +input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield;} +input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;} +textarea{overflow:auto;vertical-align:top;} +body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333333;background-color:#ffffff;} +a{color:#0088cc;text-decoration:none;} +a:hover{color:#005580;text-decoration:underline;} +.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} +.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);} +.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px;} +.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} +.row:after{clear:both;} +[class*="span"]{float:left;min-height:1px;margin-left:20px;} +.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;} +.span12{width:940px;} +.span11{width:860px;} +.span10{width:780px;} +.span9{width:700px;} +.span8{width:620px;} +.span7{width:540px;} +.span6{width:460px;} +.span5{width:380px;} +.span4{width:300px;} +.span3{width:220px;} +.span2{width:140px;} +.span1{width:60px;} +.offset12{margin-left:980px;} +.offset11{margin-left:900px;} +.offset10{margin-left:820px;} +.offset9{margin-left:740px;} +.offset8{margin-left:660px;} +.offset7{margin-left:580px;} +.offset6{margin-left:500px;} +.offset5{margin-left:420px;} +.offset4{margin-left:340px;} +.offset3{margin-left:260px;} +.offset2{margin-left:180px;} +.offset1{margin-left:100px;} +.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} +.row-fluid:after{clear:both;} +.row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;} +.row-fluid [class*="span"]:first-child{margin-left:0;} +.row-fluid .span12{width:100%;*width:99.94680851063829%;} +.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%;} +.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%;} +.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%;} +.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%;} +.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%;} +.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%;} +.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%;} +.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%;} +.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%;} +.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%;} +.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%;} +.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%;} +.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%;} +.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%;} +.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%;} +.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%;} +.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%;} +.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%;} +.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%;} +.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%;} +.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%;} +.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%;} +.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%;} +.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%;} +.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%;} +.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%;} +.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%;} +.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%;} +.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%;} +.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%;} +.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%;} +.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%;} +.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%;} +.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%;} +.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%;} +[class*="span"].hide,.row-fluid [class*="span"].hide{display:none;} +[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right;} +.container{margin-right:auto;margin-left:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";line-height:0;} +.container:after{clear:both;} +.container-fluid{padding-right:20px;padding-left:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";line-height:0;} +.container-fluid:after{clear:both;} +p{margin:0 0 10px;} +.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px;} +small{font-size:85%;} +strong{font-weight:bold;} +em{font-style:italic;} +cite{font-style:normal;} +.muted{color:#999999;} +.text-warning{color:#c09853;} +.text-error{color:#b94a48;} +.text-info{color:#3a87ad;} +.text-success{color:#468847;} +h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:1;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999999;} +h1{font-size:36px;line-height:40px;} +h2{font-size:30px;line-height:40px;} +h3{font-size:24px;line-height:40px;} +h4{font-size:18px;line-height:20px;} +h5{font-size:14px;line-height:20px;} +h6{font-size:12px;line-height:20px;} +h1 small{font-size:24px;} +h2 small{font-size:18px;} +h3 small{font-size:14px;} +h4 small{font-size:14px;} +.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eeeeee;} +ul,ol{padding:0;margin:0 0 10px 25px;} +ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} +li{line-height:20px;} +ul.unstyled,ol.unstyled{margin-left:0;list-style:none;} +dl{margin-bottom:20px;} +dt,dd{line-height:20px;} +dt{font-weight:bold;} +dd{margin-left:10px;} +.dl-horizontal{*zoom:1;}.dl-horizontal:before,.dl-horizontal:after{display:table;content:"";line-height:0;} +.dl-horizontal:after{clear:both;} +.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.dl-horizontal dd{margin-left:180px;} +hr{margin:20px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;} +abbr[title]{cursor:help;border-bottom:1px dotted #999999;} +abbr.initialism{font-size:90%;text-transform:uppercase;} +blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:25px;} +blockquote small{display:block;line-height:20px;color:#999999;}blockquote small:before{content:'\2014 \00A0';} +blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eeeeee;border-left:0;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;} +blockquote.pull-right small:before{content:'';} +blockquote.pull-right small:after{content:'\00A0 \2014';} +q:before,q:after,blockquote:before,blockquote:after{content:"";} +address{display:block;margin-bottom:20px;font-style:normal;line-height:20px;} +code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;} +pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}pre.prettyprint{margin-bottom:20px;} +pre code{padding:0;color:inherit;background-color:transparent;border:0;} +.pre-scrollable{max-height:340px;overflow-y:scroll;} +.label,.badge{font-size:11.844px;font-weight:bold;line-height:14px;color:#ffffff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;} +.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;} +a.label:hover,a.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;} +.label-important,.badge-important{background-color:#b94a48;} +.label-important[href],.badge-important[href]{background-color:#953b39;} +.label-warning,.badge-warning{background-color:#f89406;} +.label-warning[href],.badge-warning[href]{background-color:#c67605;} +.label-success,.badge-success{background-color:#468847;} +.label-success[href],.badge-success[href]{background-color:#356635;} +.label-info,.badge-info{background-color:#3a87ad;} +.label-info[href],.badge-info[href]{background-color:#2d6987;} +.label-inverse,.badge-inverse{background-color:#333333;} +.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a;} +.btn .label,.btn .badge{position:relative;top:-1px;} +.btn-mini .label,.btn-mini .badge{top:0;} +table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0;} +.table{width:100%;margin-bottom:20px;}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;} +.table th{font-weight:bold;} +.table thead th{vertical-align:bottom;} +.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;} +.table tbody+tbody{border-top:2px solid #dddddd;} +.table-condensed th,.table-condensed td{padding:4px 5px;} +.table-bordered{border:1px solid #dddddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;} +.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;} +.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;} +.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px;} +.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child,.table-bordered tfoot:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;} +.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child,.table-bordered tfoot:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;} +.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;} +.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topleft:4px;} +.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} +.table-hover tbody tr:hover td,.table-hover tbody tr:hover th{background-color:#f5f5f5;} +table [class*=span],.row-fluid table [class*=span]{display:table-cell;float:none;margin-left:0;} +.table .span1{float:none;width:44px;margin-left:0;} +.table .span2{float:none;width:124px;margin-left:0;} +.table .span3{float:none;width:204px;margin-left:0;} +.table .span4{float:none;width:284px;margin-left:0;} +.table .span5{float:none;width:364px;margin-left:0;} +.table .span6{float:none;width:444px;margin-left:0;} +.table .span7{float:none;width:524px;margin-left:0;} +.table .span8{float:none;width:604px;margin-left:0;} +.table .span9{float:none;width:684px;margin-left:0;} +.table .span10{float:none;width:764px;margin-left:0;} +.table .span11{float:none;width:844px;margin-left:0;} +.table .span12{float:none;width:924px;margin-left:0;} +.table .span13{float:none;width:1004px;margin-left:0;} +.table .span14{float:none;width:1084px;margin-left:0;} +.table .span15{float:none;width:1164px;margin-left:0;} +.table .span16{float:none;width:1244px;margin-left:0;} +.table .span17{float:none;width:1324px;margin-left:0;} +.table .span18{float:none;width:1404px;margin-left:0;} +.table .span19{float:none;width:1484px;margin-left:0;} +.table .span20{float:none;width:1564px;margin-left:0;} +.table .span21{float:none;width:1644px;margin-left:0;} +.table .span22{float:none;width:1724px;margin-left:0;} +.table .span23{float:none;width:1804px;margin-left:0;} +.table .span24{float:none;width:1884px;margin-left:0;} +.table tbody tr.success td{background-color:#dff0d8;} +.table tbody tr.error td{background-color:#f2dede;} +.table tbody tr.warning td{background-color:#fcf8e3;} +.table tbody tr.info td{background-color:#d9edf7;} +.table-hover tbody tr.success:hover td{background-color:#d0e9c6;} +.table-hover tbody tr.error:hover td{background-color:#ebcccc;} +.table-hover tbody tr.warning:hover td{background-color:#faf2cc;} +.table-hover tbody tr.info:hover td{background-color:#c4e3f3;} +form{margin:0 0 20px;} +fieldset{padding:0;margin:0;border:0;} +legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333333;border:0;border-bottom:1px solid #e5e5e5;}legend small{font-size:15px;color:#999999;} +label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px;} +input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;} +label{display:block;margin-bottom:5px;} +select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:9px;font-size:14px;line-height:20px;color:#555555;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +input,textarea,.uneditable-input{width:206px;} +textarea{height:auto;} +textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#ffffff;border:1px solid #cccccc;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear .2s, box-shadow linear .2s;-moz-transition:border linear .2s, box-shadow linear .2s;-o-transition:border linear .2s, box-shadow linear .2s;transition:border linear .2s, box-shadow linear .2s;}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82, 168, 236, 0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);} +input[type="radio"],input[type="checkbox"]{margin:4px 0 0;*margin-top:0;margin-top:1px \9;line-height:normal;cursor:pointer;} +input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto;} +select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px;} +select{width:220px;border:1px solid #cccccc;background-color:#ffffff;} +select[multiple],select[size]{height:auto;} +select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +.uneditable-input,.uneditable-textarea{color:#999999;background-color:#fcfcfc;border-color:#cccccc;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;} +.uneditable-input{overflow:hidden;white-space:nowrap;} +.uneditable-textarea{width:auto;height:auto;} +input:-moz-placeholder,textarea:-moz-placeholder{color:#999999;} +input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999999;} +input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999999;} +.radio,.checkbox{min-height:18px;padding-left:18px;} +.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;} +.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;} +.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;} +.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;} +.input-mini{width:60px;} +.input-small{width:90px;} +.input-medium{width:150px;} +.input-large{width:210px;} +.input-xlarge{width:270px;} +.input-xxlarge{width:530px;} +input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0;} +.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block;} +input,textarea,.uneditable-input{margin-left:0;} +.controls-row [class*="span"]+[class*="span"]{margin-left:20px;} +input.span12, textarea.span12, .uneditable-input.span12{width:926px;} +input.span11, textarea.span11, .uneditable-input.span11{width:846px;} +input.span10, textarea.span10, .uneditable-input.span10{width:766px;} +input.span9, textarea.span9, .uneditable-input.span9{width:686px;} +input.span8, textarea.span8, .uneditable-input.span8{width:606px;} +input.span7, textarea.span7, .uneditable-input.span7{width:526px;} +input.span6, textarea.span6, .uneditable-input.span6{width:446px;} +input.span5, textarea.span5, .uneditable-input.span5{width:366px;} +input.span4, textarea.span4, .uneditable-input.span4{width:286px;} +input.span3, textarea.span3, .uneditable-input.span3{width:206px;} +input.span2, textarea.span2, .uneditable-input.span2{width:126px;} +input.span1, textarea.span1, .uneditable-input.span1{width:46px;} +.controls-row{*zoom:1;}.controls-row:before,.controls-row:after{display:table;content:"";line-height:0;} +.controls-row:after{clear:both;} +.controls-row [class*="span"]{float:left;} +input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eeeeee;} +input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent;} +.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;} +.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;} +.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;} +.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;} +.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;} +.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;} +.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;} +.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;} +.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;} +.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;} +.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;} +.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;} +.control-group.info>label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad;} +.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad;} +.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;} +.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad;} +input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;} +.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";line-height:0;} +.form-actions:after{clear:both;} +.help-block,.help-inline{color:#595959;} +.help-block{display:block;margin-bottom:10px;} +.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;} +.input-append,.input-prepend{margin-bottom:5px;font-size:0;white-space:nowrap;}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;font-size:14px;vertical-align:top;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2;} +.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #ffffff;background-color:#eeeeee;border:1px solid #ccc;} +.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546;} +.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;} +.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-append .add-on,.input-append .btn{margin-left:-1px;} +.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;} +.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px;} +.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0;} +.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0;} +.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px;} +.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;*zoom:1;margin-bottom:0;vertical-align:middle;} +.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;} +.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block;} +.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;} +.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;} +.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0;} +.control-group{margin-bottom:10px;} +legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate;} +.form-horizontal .control-group{margin-bottom:20px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";line-height:0;} +.form-horizontal .control-group:after{clear:both;} +.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right;} +.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0;}.form-horizontal .controls:first-child{*padding-left:180px;} +.form-horizontal .help-block{margin-bottom:0;} +.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block{margin-top:10px;} +.form-horizontal .form-actions{padding-left:180px;} +.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 14px;margin-bottom:0;font-size:14px;line-height:20px;*line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333333;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(to bottom, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #bbbbbb;*border:0;border-bottom-color:#a2a2a2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333333;background-color:#e6e6e6;*background-color:#d9d9d9;} +.btn:active,.btn.active{background-color:#cccccc \9;} +.btn:first-child{*margin-left:0;} +.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;} +.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;} +.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);} +.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.btn-large{padding:9px 14px;font-size:16px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.btn-large [class^="icon-"]{margin-top:2px;} +.btn-small{padding:3px 9px;font-size:12px;line-height:18px;} +.btn-small [class^="icon-"]{margin-top:0;} +.btn-mini{padding:2px 6px;font-size:11px;line-height:17px;} +.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} +.btn-block+.btn-block{margin-top:5px;} +input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%;} +.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);} +.btn{border-color:#c5c5c5;border-color:rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25);} +.btn-primary{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(to bottom, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#0044cc;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#ffffff;background-color:#0044cc;*background-color:#003bb3;} +.btn-primary:active,.btn-primary.active{background-color:#003399 \9;} +.btn-warning{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#ffffff;background-color:#f89406;*background-color:#df8505;} +.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;} +.btn-danger{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(to bottom, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#ffffff;background-color:#bd362f;*background-color:#a9302a;} +.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;} +.btn-success{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(to bottom, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#ffffff;background-color:#51a351;*background-color:#499249;} +.btn-success:active,.btn-success.active{background-color:#408140 \9;} +.btn-info{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(to bottom, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#ffffff;background-color:#2f96b4;*background-color:#2a85a0;} +.btn-info:active,.btn-info.active{background-color:#24748c \9;} +.btn-inverse{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#363636;background-image:-moz-linear-gradient(top, #444444, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222));background-image:-webkit-linear-gradient(top, #444444, #222222);background-image:-o-linear-gradient(top, #444444, #222222);background-image:linear-gradient(to bottom, #444444, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#222222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#ffffff;background-color:#222222;*background-color:#151515;} +.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;} +button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;} +button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;} +button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;} +button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;} +.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.btn-link{border-color:transparent;cursor:pointer;color:#0088cc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.btn-link:hover{color:#005580;text-decoration:underline;background-color:transparent;} +.btn-link[disabled]:hover{color:#333333;text-decoration:none;} +[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;margin-top:1px;} +.icon-white,.nav-tabs>.active>a>[class^="icon-"],.nav-tabs>.active>a>[class*=" icon-"],.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png");} +.icon-glass{background-position:0 0;} +.icon-music{background-position:-24px 0;} +.icon-search{background-position:-48px 0;} +.icon-envelope{background-position:-72px 0;} +.icon-heart{background-position:-96px 0;} +.icon-star{background-position:-120px 0;} +.icon-star-empty{background-position:-144px 0;} +.icon-user{background-position:-168px 0;} +.icon-film{background-position:-192px 0;} +.icon-th-large{background-position:-216px 0;} +.icon-th{background-position:-240px 0;} +.icon-th-list{background-position:-264px 0;} +.icon-ok{background-position:-288px 0;} +.icon-remove{background-position:-312px 0;} +.icon-zoom-in{background-position:-336px 0;} +.icon-zoom-out{background-position:-360px 0;} +.icon-off{background-position:-384px 0;} +.icon-signal{background-position:-408px 0;} +.icon-cog{background-position:-432px 0;} +.icon-trash{background-position:-456px 0;} +.icon-home{background-position:0 -24px;} +.icon-file{background-position:-24px -24px;} +.icon-time{background-position:-48px -24px;} +.icon-road{background-position:-72px -24px;} +.icon-download-alt{background-position:-96px -24px;} +.icon-download{background-position:-120px -24px;} +.icon-upload{background-position:-144px -24px;} +.icon-inbox{background-position:-168px -24px;} +.icon-play-circle{background-position:-192px -24px;} +.icon-repeat{background-position:-216px -24px;} +.icon-refresh{background-position:-240px -24px;} +.icon-list-alt{background-position:-264px -24px;} +.icon-lock{background-position:-287px -24px;} +.icon-flag{background-position:-312px -24px;} +.icon-headphones{background-position:-336px -24px;} +.icon-volume-off{background-position:-360px -24px;} +.icon-volume-down{background-position:-384px -24px;} +.icon-volume-up{background-position:-408px -24px;} +.icon-qrcode{background-position:-432px -24px;} +.icon-barcode{background-position:-456px -24px;} +.icon-tag{background-position:0 -48px;} +.icon-tags{background-position:-25px -48px;} +.icon-book{background-position:-48px -48px;} +.icon-bookmark{background-position:-72px -48px;} +.icon-print{background-position:-96px -48px;} +.icon-camera{background-position:-120px -48px;} +.icon-font{background-position:-144px -48px;} +.icon-bold{background-position:-167px -48px;} +.icon-italic{background-position:-192px -48px;} +.icon-text-height{background-position:-216px -48px;} +.icon-text-width{background-position:-240px -48px;} +.icon-align-left{background-position:-264px -48px;} +.icon-align-center{background-position:-288px -48px;} +.icon-align-right{background-position:-312px -48px;} +.icon-align-justify{background-position:-336px -48px;} +.icon-list{background-position:-360px -48px;} +.icon-indent-left{background-position:-384px -48px;} +.icon-indent-right{background-position:-408px -48px;} +.icon-facetime-video{background-position:-432px -48px;} +.icon-picture{background-position:-456px -48px;} +.icon-pencil{background-position:0 -72px;} +.icon-map-marker{background-position:-24px -72px;} +.icon-adjust{background-position:-48px -72px;} +.icon-tint{background-position:-72px -72px;} +.icon-edit{background-position:-96px -72px;} +.icon-share{background-position:-120px -72px;} +.icon-check{background-position:-144px -72px;} +.icon-move{background-position:-168px -72px;} +.icon-step-backward{background-position:-192px -72px;} +.icon-fast-backward{background-position:-216px -72px;} +.icon-backward{background-position:-240px -72px;} +.icon-play{background-position:-264px -72px;} +.icon-pause{background-position:-288px -72px;} +.icon-stop{background-position:-312px -72px;} +.icon-forward{background-position:-336px -72px;} +.icon-fast-forward{background-position:-360px -72px;} +.icon-step-forward{background-position:-384px -72px;} +.icon-eject{background-position:-408px -72px;} +.icon-chevron-left{background-position:-432px -72px;} +.icon-chevron-right{background-position:-456px -72px;} +.icon-plus-sign{background-position:0 -96px;} +.icon-minus-sign{background-position:-24px -96px;} +.icon-remove-sign{background-position:-48px -96px;} +.icon-ok-sign{background-position:-72px -96px;} +.icon-question-sign{background-position:-96px -96px;} +.icon-info-sign{background-position:-120px -96px;} +.icon-screenshot{background-position:-144px -96px;} +.icon-remove-circle{background-position:-168px -96px;} +.icon-ok-circle{background-position:-192px -96px;} +.icon-ban-circle{background-position:-216px -96px;} +.icon-arrow-left{background-position:-240px -96px;} +.icon-arrow-right{background-position:-264px -96px;} +.icon-arrow-up{background-position:-289px -96px;} +.icon-arrow-down{background-position:-312px -96px;} +.icon-share-alt{background-position:-336px -96px;} +.icon-resize-full{background-position:-360px -96px;} +.icon-resize-small{background-position:-384px -96px;} +.icon-plus{background-position:-408px -96px;} +.icon-minus{background-position:-433px -96px;} +.icon-asterisk{background-position:-456px -96px;} +.icon-exclamation-sign{background-position:0 -120px;} +.icon-gift{background-position:-24px -120px;} +.icon-leaf{background-position:-48px -120px;} +.icon-fire{background-position:-72px -120px;} +.icon-eye-open{background-position:-96px -120px;} +.icon-eye-close{background-position:-120px -120px;} +.icon-warning-sign{background-position:-144px -120px;} +.icon-plane{background-position:-168px -120px;} +.icon-calendar{background-position:-192px -120px;} +.icon-random{background-position:-216px -120px;width:16px;} +.icon-comment{background-position:-240px -120px;} +.icon-magnet{background-position:-264px -120px;} +.icon-chevron-up{background-position:-288px -120px;} +.icon-chevron-down{background-position:-313px -119px;} +.icon-retweet{background-position:-336px -120px;} +.icon-shopping-cart{background-position:-360px -120px;} +.icon-folder-close{background-position:-384px -120px;} +.icon-folder-open{background-position:-408px -120px;width:16px;} +.icon-resize-vertical{background-position:-432px -119px;} +.icon-resize-horizontal{background-position:-456px -118px;} +.icon-hdd{background-position:0 -144px;} +.icon-bullhorn{background-position:-24px -144px;} +.icon-bell{background-position:-48px -144px;} +.icon-certificate{background-position:-72px -144px;} +.icon-thumbs-up{background-position:-96px -144px;} +.icon-thumbs-down{background-position:-120px -144px;} +.icon-hand-right{background-position:-144px -144px;} +.icon-hand-left{background-position:-168px -144px;} +.icon-hand-up{background-position:-192px -144px;} +.icon-hand-down{background-position:-216px -144px;} +.icon-circle-arrow-right{background-position:-240px -144px;} +.icon-circle-arrow-left{background-position:-264px -144px;} +.icon-circle-arrow-up{background-position:-288px -144px;} +.icon-circle-arrow-down{background-position:-312px -144px;} +.icon-globe{background-position:-336px -144px;} +.icon-wrench{background-position:-360px -144px;} +.icon-tasks{background-position:-384px -144px;} +.icon-filter{background-position:-408px -144px;} +.icon-briefcase{background-position:-432px -144px;} +.icon-fullscreen{background-position:-456px -144px;} +.btn-group{position:relative;font-size:0;vertical-align:middle;white-space:nowrap;*margin-left:.3em;}.btn-group:first-child{*margin-left:0;} +.btn-group+.btn-group{margin-left:5px;} +.btn-toolbar{font-size:0;margin-top:10px;margin-bottom:10px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;} +.btn-toolbar .btn+.btn,.btn-toolbar .btn-group+.btn,.btn-toolbar .btn+.btn-group{margin-left:5px;} +.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.btn-group>.btn+.btn{margin-left:-1px;} +.btn-group>.btn,.btn-group>.dropdown-menu{font-size:14px;} +.btn-group>.btn-mini{font-size:11px;} +.btn-group>.btn-small{font-size:12px;} +.btn-group>.btn-large{font-size:16px;} +.btn-group>.btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;} +.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;} +.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;} +.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;} +.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2;} +.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;} +.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:5px;*padding-bottom:5px;} +.btn-group>.btn-mini+.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:2px;*padding-bottom:2px;} +.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px;} +.btn-group>.btn-large+.dropdown-toggle{padding-left:12px;padding-right:12px;*padding-top:7px;*padding-bottom:7px;} +.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);} +.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6;} +.btn-group.open .btn-primary.dropdown-toggle{background-color:#0044cc;} +.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406;} +.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f;} +.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351;} +.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4;} +.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222222;} +.btn .caret{margin-top:8px;margin-left:0;} +.btn-mini .caret,.btn-small .caret,.btn-large .caret{margin-top:6px;} +.btn-large .caret{border-left-width:5px;border-right-width:5px;border-top-width:5px;} +.dropup .btn-large .caret{border-bottom:5px solid #000000;border-top:0;} +.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;} +.btn-group-vertical{display:inline-block;*display:inline;*zoom:1;} +.btn-group-vertical .btn{display:block;float:none;width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.btn-group-vertical .btn+.btn{margin-left:0;margin-top:-1px;} +.btn-group-vertical .btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;} +.btn-group-vertical .btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;} +.btn-group-vertical .btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0;} +.btn-group-vertical .btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;} +.nav{margin-left:0;margin-bottom:20px;list-style:none;} +.nav>li>a{display:block;} +.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;} +.nav>.pull-right{float:right;} +.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;} +.nav li+.nav-header{margin-top:9px;} +.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;} +.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);} +.nav-list>li>a{padding:3px 15px;} +.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;} +.nav-list [class^="icon-"]{margin-right:2px;} +.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;} +.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";line-height:0;} +.nav-tabs:after,.nav-pills:after{clear:both;} +.nav-tabs>li,.nav-pills>li{float:left;} +.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;} +.nav-tabs{border-bottom:1px solid #ddd;} +.nav-tabs>li{margin-bottom:-1px;} +.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;} +.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;} +.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;} +.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;} +.nav-stacked>li{float:none;} +.nav-stacked>li>a{margin-right:0;} +.nav-tabs.nav-stacked{border-bottom:0;} +.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;} +.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;} +.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;} +.nav-pills.nav-stacked>li>a{margin-bottom:3px;} +.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;} +.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;} +.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} +.nav .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;} +.nav .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;} +.nav-tabs .dropdown-toggle .caret{margin-top:8px;} +.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff;} +.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555555;border-bottom-color:#555555;} +.nav>.dropdown.active>a:hover{cursor:pointer;} +.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;} +.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);} +.tabs-stacked .open>a:hover{border-color:#999999;} +.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";line-height:0;} +.tabbable:after{clear:both;} +.tab-content{overflow:auto;} +.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0;} +.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;} +.tab-content>.active,.pill-content>.active{display:block;} +.tabs-below>.nav-tabs{border-top:1px solid #ddd;} +.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0;} +.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below>.nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;} +.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd;} +.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none;} +.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;} +.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;} +.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;} +.tabs-left>.nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;} +.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;} +.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;} +.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;} +.tabs-right>.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;} +.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;} +.nav>.disabled>a{color:#999999;} +.nav>.disabled>a:hover{text-decoration:none;background-color:transparent;cursor:default;} +.navbar{overflow:visible;margin-bottom:20px;color:#777777;*position:relative;*z-index:2;} +.navbar-inner{min-height:40px;padding-left:20px;padding-right:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top, #ffffff, #f2f2f2);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2));background-image:-webkit-linear-gradient(top, #ffffff, #f2f2f2);background-image:-o-linear-gradient(top, #ffffff, #f2f2f2);background-image:linear-gradient(to bottom, #ffffff, #f2f2f2);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0);border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);-moz-box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);*zoom:1;}.navbar-inner:before,.navbar-inner:after{display:table;content:"";line-height:0;} +.navbar-inner:after{clear:both;} +.navbar .container{width:auto;} +.nav-collapse.collapse{height:auto;} +.navbar .brand{float:left;display:block;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777777;text-shadow:0 1px 0 #ffffff;}.navbar .brand:hover{text-decoration:none;} +.navbar-text{margin-bottom:0;line-height:40px;} +.navbar-link{color:#777777;}.navbar-link:hover{color:#333333;} +.navbar .divider-vertical{height:40px;margin:0 9px;border-left:1px solid #f2f2f2;border-right:1px solid #ffffff;} +.navbar .btn,.navbar .btn-group{margin-top:5px;} +.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn{margin-top:0;} +.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";line-height:0;} +.navbar-form:after{clear:both;} +.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;} +.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0;} +.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;} +.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;} +.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0;}.navbar-search .search-query{margin-bottom:0;padding:4px 14px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;} +.navbar-static-top{position:static;width:100%;margin-bottom:0;}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;} +.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px;} +.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0;} +.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;} +.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;} +.navbar-fixed-top{top:0;} +.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);} +.navbar-fixed-bottom{bottom:0;}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);} +.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;} +.navbar .nav.pull-right{float:right;margin-right:0;} +.navbar .nav>li{float:left;} +.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777777;text-decoration:none;text-shadow:0 1px 0 #ffffff;} +.navbar .nav .dropdown-toggle .caret{margin-top:8px;} +.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{background-color:transparent;color:#333333;text-decoration:none;} +.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);-moz-box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);} +.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#ededed;background-image:-moz-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5));background-image:-webkit-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:-o-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:linear-gradient(to bottom, #f2f2f2, #e5e5e5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0);border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e5e5e5;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#ffffff;background-color:#e5e5e5;*background-color:#d9d9d9;} +.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#cccccc \9;} +.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);} +.btn-navbar .icon-bar+.icon-bar{margin-top:3px;} +.navbar .nav>li>.dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;} +.navbar .nav>li>.dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;} +.navbar-fixed-bottom .nav>li>.dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;} +.navbar-fixed-bottom .nav>li>.dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;} +.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e5e5e5;color:#555555;} +.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777777;border-bottom-color:#777777;} +.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555555;border-bottom-color:#555555;} +.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{left:auto;right:0;}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{left:auto;right:12px;} +.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{left:auto;right:13px;} +.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{left:auto;right:100%;margin-left:0;margin-right:-1px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px;} +.navbar-inverse{color:#999999;}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top, #222222, #111111);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));background-image:-webkit-linear-gradient(top, #222222, #111111);background-image:-o-linear-gradient(top, #222222, #111111);background-image:linear-gradient(to bottom, #222222, #111111);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);border-color:#252525;} +.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999999;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover{color:#ffffff;} +.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{background-color:transparent;color:#ffffff;} +.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#ffffff;background-color:#111111;} +.navbar-inverse .navbar-link{color:#999999;}.navbar-inverse .navbar-link:hover{color:#ffffff;} +.navbar-inverse .divider-vertical{border-left-color:#111111;border-right-color:#222222;} +.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{background-color:#111111;color:#ffffff;} +.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999999;border-bottom-color:#999999;} +.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;} +.navbar-inverse .navbar-search .search-query{color:#ffffff;background-color:#515151;border-color:#111111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#cccccc;} +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#cccccc;} +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;} +.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;} +.navbar-inverse .btn-navbar{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e0e0e;background-image:-moz-linear-gradient(top, #151515, #040404);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404));background-image:-webkit-linear-gradient(top, #151515, #040404);background-image:-o-linear-gradient(top, #151515, #040404);background-image:linear-gradient(to bottom, #151515, #040404);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0);border-color:#040404 #040404 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#040404;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#ffffff;background-color:#040404;*background-color:#000000;} +.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000000 \9;} +.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;} +.breadcrumb .divider{padding:0 5px;color:#ccc;} +.breadcrumb .active{color:#999999;} +.pagination{height:40px;margin:20px 0;} +.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);} +.pagination ul>li{display:inline;} +.pagination ul>li>a,.pagination ul>li>span{float:left;padding:0 14px;line-height:38px;text-decoration:none;background-color:#ffffff;border:1px solid #dddddd;border-left-width:0;} +.pagination ul>li>a:hover,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5;} +.pagination ul>.active>a,.pagination ul>.active>span{color:#999999;cursor:default;} +.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999999;background-color:transparent;cursor:default;} +.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.pagination-centered{text-align:center;} +.pagination-right{text-align:right;} +.pager{margin:20px 0;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";line-height:0;} +.pager:after{clear:both;} +.pager li{display:inline;} +.pager a,.pager span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;} +.pager a:hover{text-decoration:none;background-color:#f5f5f5;} +.pager .next a,.pager .next span{float:right;} +.pager .previous a{float:left;} +.pager .disabled a,.pager .disabled a:hover,.pager .disabled span{color:#999999;background-color:#fff;cursor:default;} +.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";line-height:0;} +.thumbnails:after{clear:both;} +.row-fluid .thumbnails{margin-left:0;} +.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px;} +.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;-o-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;} +a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);} +.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;} +.thumbnail .caption{padding:9px;color:#555555;} +.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;} +.alert h4{margin:0;} +.alert .close{position:relative;top:-2px;right:-21px;line-height:20px;} +.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;} +.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;} +.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;} +.alert-block{padding-top:14px;padding-bottom:14px;} +.alert-block>p,.alert-block>ul{margin-bottom:0;} +.alert-block p+p{margin-top:5px;} +@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-o-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.progress .bar{width:0%;height:100%;color:#ffffff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;} +.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);} +.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;} +.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;} +.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(to bottom, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);} +.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(to bottom, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);} +.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(to bottom, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);} +.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);} +.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);} +.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;} +.hero-unit p{font-size:18px;font-weight:200;line-height:30px;color:inherit;} +.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);} +.tooltip.top{margin-top:-3px;} +.tooltip.right{margin-left:3px;} +.tooltip.bottom{margin-top:3px;} +.tooltip.left{margin-left:-3px;} +.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid;} +.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000;} +.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000;} +.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000;} +.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000;} +.popover{position:absolute;top:0;left:0;z-index:1010;display:none;width:236px;padding:1px;background-color:#ffffff;-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);}.popover.top{margin-bottom:10px;} +.popover.right{margin-left:10px;} +.popover.bottom{margin-top:10px;} +.popover.left{margin-right:10px;} +.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;} +.popover-content{padding:9px 14px;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;} +.popover .arrow,.popover .arrow:after{position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid;} +.popover .arrow:after{content:"";z-index:-1;} +.popover.top .arrow{bottom:-10px;left:50%;margin-left:-10px;border-width:10px 10px 0;border-top-color:#ffffff;}.popover.top .arrow:after{border-width:11px 11px 0;border-top-color:rgba(0, 0, 0, 0.25);bottom:-1px;left:-11px;} +.popover.right .arrow{top:50%;left:-10px;margin-top:-10px;border-width:10px 10px 10px 0;border-right-color:#ffffff;}.popover.right .arrow:after{border-width:11px 11px 11px 0;border-right-color:rgba(0, 0, 0, 0.25);bottom:-11px;left:-1px;} +.popover.bottom .arrow{top:-10px;left:50%;margin-left:-10px;border-width:0 10px 10px;border-bottom-color:#ffffff;}.popover.bottom .arrow:after{border-width:0 11px 11px;border-bottom-color:rgba(0, 0, 0, 0.25);top:-1px;left:-11px;} +.popover.left .arrow{top:50%;right:-10px;margin-top:-10px;border-width:10px 0 10px 10px;border-left-color:#ffffff;}.popover.left .arrow:after{border-width:11px 0 11px 11px;border-left-color:rgba(0, 0, 0, 0.25);bottom:-11px;right:-1px;} +.modal-open .modal .dropdown-menu{z-index:2050;} +.modal-open .modal .dropdown.open{*z-index:2050;} +.modal-open .modal .popover{z-index:2060;} +.modal-open .modal .tooltip{z-index:2080;} +.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;} +.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);} +.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} +.modal.fade.in{top:50%;} +.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;} +.modal-header h3{margin:0;line-height:30px;} +.modal-body{overflow-y:auto;max-height:400px;padding:15px;} +.modal-form{margin-bottom:0;} +.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";line-height:0;} +.modal-footer:after{clear:both;} +.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;} +.modal-footer .btn-group .btn+.btn{margin-left:-1px;} +.dropup,.dropdown{position:relative;} +.dropdown-toggle{*margin-bottom:-3px;} +.dropdown-toggle:active,.open .dropdown-toggle{outline:0;} +.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000000;border-right:4px solid transparent;border-left:4px solid transparent;content:"";} +.dropdown .caret{margin-top:8px;margin-left:2px;} +.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#ffffff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;}.dropdown-menu.pull-right{right:0;left:auto;} +.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;} +.dropdown-menu a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333333;white-space:nowrap;} +.dropdown-menu li>a:hover,.dropdown-menu li>a:focus,.dropdown-submenu:hover>a{text-decoration:none;color:#ffffff;background-color:#0088cc;background-color:#0081c2;background-image:-moz-linear-gradient(top, #0088cc, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));background-image:-webkit-linear-gradient(top, #0088cc, #0077b3);background-image:-o-linear-gradient(top, #0088cc, #0077b3);background-image:linear-gradient(to bottom, #0088cc, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);} +.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;outline:0;background-color:#0088cc;background-color:#0081c2;background-image:-moz-linear-gradient(top, #0088cc, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));background-image:-webkit-linear-gradient(top, #0088cc, #0077b3);background-image:-o-linear-gradient(top, #0088cc, #0077b3);background-image:linear-gradient(to bottom, #0088cc, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);} +.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:#999999;} +.dropdown-menu .disabled>a:hover{text-decoration:none;background-color:transparent;cursor:default;} +.open{*z-index:1000;}.open >.dropdown-menu{display:block;} +.pull-right>.dropdown-menu{right:0;left:auto;} +.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"";} +.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;} +.dropdown-submenu{position:relative;} +.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;} +.dropdown-submenu:hover>.dropdown-menu{display:block;} +.dropdown-submenu>a:after{display:block;content:" ";float:right;width:0;height:0;border-color:transparent;border-style:solid;border-width:5px 0 5px 5px;border-left-color:#cccccc;margin-top:5px;margin-right:-10px;} +.dropdown-submenu:hover>a:after{border-left-color:#ffffff;} +.dropdown .dropdown-menu .nav-header{padding-left:20px;padding-right:20px;} +.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.accordion{margin-bottom:20px;} +.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.accordion-heading{border-bottom:0;} +.accordion-heading .accordion-toggle{display:block;padding:8px 15px;} +.accordion-toggle{cursor:pointer;} +.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;} +.carousel{position:relative;margin-bottom:20px;line-height:1;} +.carousel-inner{overflow:hidden;width:100%;position:relative;} +.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;} +.carousel .item>img{display:block;line-height:1;} +.carousel .active,.carousel .next,.carousel .prev{display:block;} +.carousel .active{left:0;} +.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;} +.carousel .next{left:100%;} +.carousel .prev{left:-100%;} +.carousel .next.left,.carousel .prev.right{left:0;} +.carousel .active.left{left:-100%;} +.carousel .active.right{left:100%;} +.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;} +.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);} +.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:15px;background:#333333;background:rgba(0, 0, 0, 0.75);} +.carousel-caption h4,.carousel-caption p{color:#ffffff;line-height:20px;} +.carousel-caption h4{margin:0 0 5px;} +.carousel-caption p{margin-bottom:0;} +.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);} +.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} +.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;cursor:pointer;opacity:0.4;filter:alpha(opacity=40);} +button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;} +.pull-right{float:right;} +.pull-left{float:left;} +.hide{display:none;} +.show{display:block;} +.invisible{visibility:hidden;} +.affix{position:fixed;} +.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;}.fade.in{opacity:1;} +.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;}.collapse.in{height:auto;} +.hidden{display:none;visibility:hidden;} +.visible-phone{display:none !important;} +.visible-tablet{display:none !important;} +.hidden-desktop{display:none !important;} +.visible-desktop{display:inherit !important;} +@media (min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important ;} .visible-tablet{display:inherit !important;} .hidden-tablet{display:none !important;}}@media (max-width:767px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important;} .visible-phone{display:inherit !important;} .hidden-phone{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade.in{top:auto;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12, textarea.span12, .uneditable-input.span12{width:710px;} input.span11, textarea.span11, .uneditable-input.span11{width:648px;} input.span10, textarea.span10, .uneditable-input.span10{width:586px;} input.span9, textarea.span9, .uneditable-input.span9{width:524px;} input.span8, textarea.span8, .uneditable-input.span8{width:462px;} input.span7, textarea.span7, .uneditable-input.span7{width:400px;} input.span6, textarea.span6, .uneditable-input.span6{width:338px;} input.span5, textarea.span5, .uneditable-input.span5{width:276px;} input.span4, textarea.span4, .uneditable-input.span4{width:214px;} input.span3, textarea.span3, .uneditable-input.span3{width:152px;} input.span2, textarea.span2, .uneditable-input.span2{width:90px;} input.span1, textarea.span1, .uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12, textarea.span12, .uneditable-input.span12{width:1156px;} input.span11, textarea.span11, .uneditable-input.span11{width:1056px;} input.span10, textarea.span10, .uneditable-input.span10{width:956px;} input.span9, textarea.span9, .uneditable-input.span9{width:856px;} input.span8, textarea.span8, .uneditable-input.span8{width:756px;} input.span7, textarea.span7, .uneditable-input.span7{width:656px;} input.span6, textarea.span6, .uneditable-input.span6{width:556px;} input.span5, textarea.span5, .uneditable-input.span5{width:456px;} input.span4, textarea.span4, .uneditable-input.span4{width:356px;} input.span3, textarea.span3, .uneditable-input.span3{width:256px;} input.span2, textarea.span2, .uneditable-input.span2{width:156px;} input.span1, textarea.span1, .uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}} diff --git a/taiga/base/api/static/api/css/default.css b/taiga/base/api/static/api/css/default.css new file mode 100644 index 00000000..d7c5722e --- /dev/null +++ b/taiga/base/api/static/api/css/default.css @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 Andrey Antukh + * Copyright (C) 2015 Jesús Espino + * Copyright (C) 2015 David Barragán + * 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 . + * + * 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; +} + diff --git a/taiga/base/api/static/api/css/prettify.css b/taiga/base/api/static/api/css/prettify.css new file mode 100644 index 00000000..c037439d --- /dev/null +++ b/taiga/base/api/static/api/css/prettify.css @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 Andrey Antukh + * Copyright (C) 2015 Jesús Espino + * Copyright (C) 2015 David Barragán + * 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 . + * + * 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; +} diff --git a/taiga/base/api/static/api/img/glyphicons-halflings-white.png b/taiga/base/api/static/api/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf6484a29d8da269f9bc874b25493a45fae3bae GIT binary patch literal 8777 zcmZvCbx;)C_xI8uA|W7xq_j#%h^VwEi1aQcAh9Cd-O?@H9STb~NP~34f^_H7uax#!+fb7$_H2zAwW6n7Z!0000ArS}S&0015o0088Y5&!^z zw3#|@004hMT}AsX4u=E$i<_I9zcLPo!(y?vwzdGkKk{Gv)xqI#XJ==(x3|~V*8l(j zgTefF9f!j$E-nH9fS#V7tE;QOyRNRT5D3J7kEpAw!(y=j0AO=-v%0$auMQfG-rL)| zy}kXbc6@vc001T?CIA2c7K^>UzCJ!a{;PvRp*A))5)u+_|3!CqcUoGSi;K&~#>V92 zBme-oyuAGN>sNk$en3C~7K@#ooi#Nz#o=&JC=`iAE-x>mP$)DSeQ!61=H7z~ENU>X}6`}_NMc6LrqPT+7j7K;Ug!4?)4XJ==Pjg9N;>j(q_g+d)1 z9PI4upin3z5;-z5g2Um0gM-h{&pkXm5D3J|%F4~n&EVkR^z`)6(Gd!TT3ub;+}zyV z-MzfL%*@RE`SWLSaq;r<^2x~w4u?BGKOY(zIy*c28_nr{hlhs;2M2#^3F)Nv zPdF}IG*z_#jDXn*e9H63RHwPdcZdW_&-&>$83;OA?eA+ftx1Re_9uQIzd>QG5LeX` z)1^kEMQKc$SDaBBJwq)%xh^Hj?q@>&Lq?co%9zKVrq8ZmIx7Bqz{k-1M3EXf4XgA< z2W~EQI-7=G4qZp5R{_kaY_bO5UVn}-vGo-V5fKzp3Mc%jF8`5ToR{C?1Gk<=$vXx0 zr}uUOKPPzOfz*qhJv{X2=KH$xA`C2Scon|Jq|;f)nHna{5M^59d5;>%KT%6|LbD1Mxb8?a@K4#@lXv7C{anU)wEPesEeVN*3){|H~GXdXMVW+t+s$BR#6_oH8 z%F>1>$w=9$^qCtcy;~@|N62e&2isiDYqdM>wp23JE2E&5$I?SQ@*#uq`j1Ov9dp7Mj=Gp0 zm`>05aJEY>^F{OszU|;bY}yfNwm7EBuYYAAwQCsSf?LjvCRVgIHm>>F1lit^w#1ol zn)h-9JpJ>qf_?z-x%~Y(DIJHCfLDH7RBX2&_6WmkdZw`qlcYk02?LyYAnG|Y+~b@aBD(#6Zxx6*A!E* zc`#tLRyJr3B&|V9j~8;jZ#ZdwSgKRg;m-*~^MtVW|9-#x_K^rbw4ybjp#ezp$@j1V z>mSNHeTSil#*5}F$lBLpz!mb>GS^R^2<@Lfi-%^`C6D%E>Nu#ja<*#}_L8v7kRW57 zCsCD2!vY!?)4%zkGBgluVt;$Ud(wig%rVmjXTs)Ln;g7Y?fTLt1>9AICV;Sy;JL1C z;bRjbij~7w5gWbUaj#LUme}mst%QunOzf)+WTLl(%JqE`Q@nua3$q4RJh$|Sj(aYK z6|^QzY1--c2k_RBWYdT^fC3T4c*uI&hK97!vnVK~F^LhUB;F_7(JF{6j}wxt>T~pl z*DYM1$1zU4MgMF*G;AItCk8qiJz@6@t#1?AUi*-R+p6g_T_*$(yNO1hvv{1r4hyL_ zH4T|eY*LR|*=KK+mh8jSBV>NN6mNqFJd;{asMl3aaSLUXwl)omok~A}ralJz)s4h9 z)?gm~4juCoBF3&2^YywjhEs*6nQY%rI+;PnI&0bWH;qM3ld6l->BuFwRS0wUQmro6 zk60nsuxoo)Oa5StGKirmpuZ%{lq=NtYU@F`rI=E&o{ehm+h^kEZ2QlCZJ1`RWm*J|)3YLPe0S=akRwB<=(93DTkEqcw-ceNZHW*VjKbJWjM z0_AL&PRlL4yqweImMT6}qi~?ig12UL+=Lv8udtNUPhV5+q1s()ex4L-Rm7@(DxI%V zi9WG~B0mk{kJC&I_c_Uw5Q^lu#%SL$eSw;B5fBq_EWH{OsdW;cc=FAPIa=s$?NqZ)FcJfqE!V`m7=H)jo-8fcqqP2nqp^U8+%i{ zYbj~p?Z=dm-^Av5E8UpE)ASDw#uEP*D^m7<3A6xNrD`}4>M8vtk!D<>hIvawpUg_@ zB@cYauus8NT3Px}1oYJ_>83o74y zBFEO#R9r}s!h|}iyFAG0Lu%AbE+a-=!&&fUvVO7e{rV8PNpO!!eZm~-@zq^eYM#?- zJf<$u?ZNkJee?Bws&tQY2vjQrC`1Sx1N_ z*|TxWEe$e>&b>cqb(2q%IrBiSWXqNOl3O6vsqq`4^ZUed)%g*Fy^|`g z*eoZdRy6!&Ozg>p>2OWPl-1+d+(N&cpzi4j0%`kNgfB~ce9)Qi3ujq%TS+BQ%Nx67 zMAaSkt6QjdO{VwQ%Gb8>x9bIEg%Ws0&VYR?S3tm+y-FnmY*71*^E?<%@(Q$qYN>E$ zf?u0C9x2?%BPkI%LpGn^#gHsfueW`p6cJD$hRB7}LJqKyIsl`bC}5QPhz`W_XZfx+ z^KY`xaJ=<)g;OGcI4(UWW1{KO5$kzlg(i^zWk*1F`C)Se$GTkDI>JmDXUL~jD<{>o=5Q__yPsY&=J{z#dwNB^L4l8)wOkAHXG_KBROe>FjL6+#mA!5&!2)=7u*9 zQ8w)eKB+-hXxc}xs}@s6X46ll3*&J$M7Rp3(74q1yFGX5@uMy)FWlh@z#)6T_0}8z zXVKiovW@F^NkX;r?n+4iC?CYRh>Jl(W^0Ud^|j%bOfs5)JP$Rl5QMtC=~t)ch0v>8 zC?lXPcOMH-|IjoHbIAE3)~?zM5iw$Bwuju5qS_1(!L`c6huuY%lX7WUqT-_WLj#Ol z@K60?!dw?>Z-H~slJ7)$I^!;$Ki4~S7>9+Ax|Y1-VGvaJTo9GczugyLhVE*~1*;oV zO2yMm!5%VYDBUF!b`cp*M96IqRqXzkpv&$5OJ~R~aj)S?9s9ia3liS40Soe~qT;AX z&!B3V(4@P}qEZ@*7i?=#5Ja)U*GP9h>}atqJOOGI)4);!rbj#%CsWC(o8-#5(5rv9 z`4x{GU6*0Q7$INbTfsN8`T9;c`{LWYtFwW(?dp5_bUOO!?a`no)VDG{p;Qx^6k^2Q zGWMfeV<;gyRczzv>kp!-qbD*>bo1Zkk(#dF=|9W)ze zj{3o?38hX)GxW;+d!BNKyPhODGt}XVUQ?8CMOxJY&H}2q_sph)gHWnEe(ERLR?s-d zi>a9LF1AGobDH%^CvaUixQf806^z$}f7Ex~!P+OB2pwizG$&mbGB<)hiDwi1)I7AtY3g4A)0K5dfpJD6_X(Cb(x^}F*m9f2n3(~`~h z>#Pu`V>!Cp_Wg@%w+%GxOGBjhV1tH>toHj!{cvmMuCmDf3PFHzbu&~e_oU5k1Ko0` z#+9DO(DcU8$M!QsF66u~uzR8_pGEnp8s~AhxvOE5K9zGNMVF$=nq+C;4GNWM0!K!5 zMGd4w@AZr_YKuDm267hL<2Aeolo3I7Y0`$iR zU|Ug<6=>^|GFK^ZEi!vet~yr5?8BD_8>aJg*HZK*jEQfiO*8#iTQm(#-fzLe8?)U)qVO{Vz5hHhx`yI^6_xf>auo zGtjphsu*>q{s`hyj{T(&)Q_-a9ja)ZV0kw+n@zD+UnvM!;(Ej57};CQH@I-ICm9{5 zG4i@oH(vmh7*%80TG&fJw<>DR< z`;psILS?!gY`Z4B`PJvjXNZK}(ATd;h!NRE^wCNdP|#J6KTSo$%c{Hyh+OHeP^MNF zmmnFH=k|0O9G#BMk{phIlsso>V zxsKKB4FL}Ax4PzWY!rcaXC;#Q*v;ID8~ESyG7*m}^9}C|v5((3(BY`CVl<|k?cJCx z0P7mhPZH_My(&U5RoPdEstNf?mrJU*%WskM3e+Aq)-V|~QOCx|U$E;q7ILjI&+^-V zd%N`b-lo4c3jeU9-=du?E;PqXAaDi`d<(zNaOcd<6_VG@*J^#4J^ zC+7F9Gq2o8Xp!ylj=VLaMM?>ABO`ch)RQl!HnnD~u`_P5~R4A^hWy8h!p(ODfxVcaoiB1~++e#E9?QWUO|CfNbZJG;W{|U% zAE#@0rEN@>J1<{%y;D*vi+!CCze3<~8q|_qq+OCh&CY;cu7TZoI81KaL29Lt;Jx;O z@Wnp)t80mm^Hz4laYgsyXNpeSwjF64R>_6GUKoYHZh`*ge>(qv`X9QyhE8#W@;W-e zNz|;%5@vmy*N{rm+IkZ2A(ggY<2*k!GvbeCyNbn)P4tkAD`mMLz)(94xz*Iq4VryD zpQ{*~-gc>vP*g0IT&K0Ska(ceDIq!SQY}&en>6wHl%6gf_q?TjvgUcL_>zYD@dFxs z^?*wg+Yf;^OUvGSGN7F>!2o69BgH3p0aWFkFSY|B4AgWWY6iPE6b#6g_z2IKx&fdb zB!+;sEIAy`E-iO8sKCsXO`;H(u4C0E^^8u|Qrx4^i|-e790(vM(`>&ft4Vlq|E@WF zfiY5sy1^nW{2l>-8g`oeDb(5i4d*Oy1p1`wSmfyp&eEzOX-Svd8WE2-@} zKCH=0jw)MI?;c3RxotLY9u5`;r`gn7eJ^czu76BV-S4uz_GE`H8dN+hTP|+#I#@7v zmr8?z>f3wLiajl-hvS8bQ;d6a=DvS!^x_=XXDti^ob?R%UilLmD#GONmC1;aw&}&p zKvQDG#^p;zb|mZ<_^dO8nRw6`b8(22HaY*uaA?90pcikkEgyQbQeJ`LbJ*N_uwaM4 zx#V7|*L~IE!Iuse_7vQzgxT^X^Fh(8VQnl@jXE<(Xti;|`FpFpbG{;7u()C{An%1k zf^EmM{m(peW}&~9=>(vZkf+x{U><-wQdg%dHhC+Jp3CF6JjR$S;2NbyyB*$n&#$K>pDvHF`ylkI_$@+2zuIzmI_2k4p zW)MTNXYhN!U8zQl_8Ku%;hL4z zL$`iVTSM70xNEVejEsI8D#XMGvi$w6p`f>Azx#hYt_E5lT>ykXz^8t4{-*{#P^zwV z**VJ4h_HqC4ayIgQU@RL>7lDejbH zCW##i=*%-7!A1Ugmruf9v}NAO9TkZP0(TSla7po2bLAheh`FJ+q{kvr z4iqjn(a|*~&ZKbYquknUsE#pkZM9E3fWVmNrML8%2_!UxXtun!?OW$QRP!%_G*%QFwV@16o9UrE1yZHg|XTIlP1aXlJYZt|_ zkiH8!M$oS_G`lV7Sl3o)iGMd1I>?X7f6wzNwF(bXUVB;Yih^k!y=|Jc#V=QRF!$?x zN<4tIZgK|`_v#6~H?s|X=CIgO`@qGw1^oooJ#=8(QRm*3RAxbh&Uxgat zX8sK6)aqeBlkxwig`m~~>;PR_YUgmjIJ>>ZrEMLodaVFU;OGN0%tahcd9{9&HcOxD zkB)1Ay1mgr(2L8hC5-#og9y1wO}Q9+d@_2$j zb@Bc5F#UsLyj??F-TsfMQS$~3DWYU=&5BCM5NrtB>at|Vn=;`w@qk@x{)PRW`BTDx zc_naV>`A&i04`!UyJxzbin+f%AkiY`({I_7J(0J2sJR4KRJI;BVXUYX_3|H9RjRXV#`cAS{qQQ=SA;XshzW2;(K74rK zHeEA}w(pIj{T&>Ar>m92Tyf~n+^vMcp0@8ygCf(!vm2$6RnS?44ah*fJGHK@j!ywZ zE?>?nu~O#iW34|rK|XgETSMIMO<9vXG^Cf~bHRM-;pDV1`~VuJ>4yg`Tajx-3V>tC8F&PnD?Jfz8bvRJk-Ylck3uh}b>rdEth~lq z8Tv_C1Iz@iNqpWeeTxCMX)OB3xGtTkiMa246_980iFlE8GZ*!Z6jqKKcE?XgqjEVm zl-#09+JP#6*uNH}FC1oeo}8CftvK&lCtN;~HFHw^c6Gbmo-q|15Eue_D|#)2@=ViR zo@VgdVHEWmZU$}DRdY3+KRsw!L-T#1L-uwNIlA;Zq`8*(ZBbB>uOeegW2gABiKRqF zyK_R4JJ^YX<=SfzoJdG7<%IJ1CcfGgZwXV=bz!Eyob-ouBOAJkJm@f7)6%opy^V(fkWW)4)eCHRWPqW?oW_+_i z^~0I0|H%Z7DEa`*rUlub0hjr#Y>_gxxStmccx<)~uR33y+e^)PE`jc7y~)@iE%+tn|6uLsrRs8a*;$Q5$@33D zT03|$X=e|P)Qc8oh!XY;1qCBB-P7)zp9H`knF$fFC*`T9`qaTJkA+SIB zO?DGe{7S;OYlY*3_?x$EZ?u@nK@|+2QbE$EI zbEz4rAu(#cTl2YW&(tue7gFHbMQiyI@%x4DCd^mHw=jSC?7I{NQ9kXPq?s?{@I!eAo$Ky=ie}Lrqmf_~JkMww7oQL$Uu=8$YL7>; zN=4P^VYURTKeOQyV<6bX9HxJlT$rbF8BqQ3dk!nYD3ov!lg@~43sb3MVTpH^Gskez zkj^~$SkUx&WlQVBb&SM(Q7r;;)XE=JhR%^Dvv>oNCgVLdWYdG-RZQQw)rI!0Q~0 zipkHz8QgpH&%gv5>C#VAd{k;(SdNLJ0A12g!%|X&;;r+tb zIuZ)WF*{+lA4DwV_(Wg$oSgsUjuRKKOeemeX`UnRM7%v+Wmab~`uxs=lTjL%vK)Z< zti7v}vFSMHLHDk+r1dt-IY;+GzeGY%twSkd>C$5#S#~ZBbmj3VK=P_*~~R`pGGqr5zSqiD+^fZ&QFF>IMX){2qhOL&EwnxAucT&5Xwa zw972*z2)xEwMtu{`T)jDssjI~b^hmrx2MYQ+0A#+Fpb*Y0-wuIU7NM<6bx!neEs>Z zVeNF^%3%XWn2)4FycJwVbqCd$C$hQd8jg+gk8J`OjhKbFu?@lwhFigQ>o3=0=K1L? zV$g8ry19?tc*tEzY3=LeXin3ff=tyh39jpaFjkCAfZz9&r=+jp;v#(y>`pSyHu=t; zYCN>vRkeQ|$3Ccz%rElh$*&%(lsp)D2^z(hmK%eza=DnyjEod2I&EYRVckrdQ`wb{ zh8xFzX5qj&μ$=cRF8EySH18_z`fwKEEj488`0Jj!~W#6wqD)i2VFf)b?e>OD?! zhL^uFmk-|X`LN>gcIuoa@lwjf4@OzcLViCzH{5%BP4b%TrL_e&%=1BshIEAondhTV zR_Q~1pbvugVz}F|Lq1`1;k(VYw~I(;P&3~fpp+U}SFlfG9SvRpC8vVNuYXgt8LI04 zZfr_)yoxIC>C}1XKW1}ykk@ygQK_md*eethKQWYy^N?mYTo=TIb!yheD)^Do-?aLq zM3zYSP9J|j`*nY~)=|zOS;n689tK4j1OTvi1$UXcLV{WVb z#qZikj?Q4|)>lDAevbG?BNqC~`kKh<$q!QDQD%|MQsEEGF=^F9PC8ZN8mkXXu`h$H z+UTpafDCekWvVZM_IgwS?xzOyouvkxCVV~VehfRC&v_u5-? zojKok_Bod0kETK=cgJ-^e9f^Tz$YC&VF63(dB0)S#zrh6^m^Rn-Zq2Wbfq=h-7I+-JpqA3Fj3I3)&vXc`Pdsc&^k2~ zexi6$J97~J{9KTFQ6f0epGZM*<}K;xV4SDHE%^bR_~Lus1mG+siIKhh_QKk4kpm`~ zNTbh;&`-hPogPBN%f9xmnJQ0iTW!(?D3=G~5^VjfmJPj|d!?l{dOus%YXbI^s27v+ zZ&{afg|p#l6ega<_E_oj`K>Rx2!V5a{bmz_LZUwxpvTvm1LGCXY<%X$eCk;KW*iL? zUUzx%l63Y)(k%3vmC84KFPkC9&5zSj+oQM&DRTGfD>JN+)ay&D327;%}WUW+#C0v zbdtsklKjoxMH&DFNVDF;>b2-SD`{$Rp~ngq3qVCniW zW2`IG1^|^pihgL~cyuxr&rW&PDToH(5a(iT-U(uxmk&)aRD^&rl_Y1Gx5UO-g17IyvYTj%;Hw z4T#uJrt-YW@YlS5Z@>-H8$r&Wr{_fn;B>|Nw;3_Rz(>U4bv2)8Z#$R*(=>c(*R>i z_o~#^^*d@NMwY0C7FQ>*f~k!_=TPi>j&?1;lkhJVx~R>p%ZE3@be=8yoMbcl=Ic-b_Uiq+VH@FHm zWk5rDIe-wP5l%$pd&B@wuY2u#@q)v@Jdrgsqp>q-Zf=rZ3|UNn3Mk9%xXh>Nbez458$FgJdK{m)P;86I&jzWU>f z5AMO|q#pWd7V~v{2di)XocW_~m&E6-hDeq@{v)OcS~o-tL#O>KAt8~O%jA1!w+PdO z58wIY;c7q{y+0nhz>dS0UizdiRLQau?ZC=9za?qVZJtzns%GudWpW(+_|~<(T^iHn zB2>GlODl9}k%r)BuB^%(#s01A`pbejPNl@3V9~OdE>%3hN@=R&KOh88ymh8e6!{8;*Y~Q?oiItZ9oB5oM%C$sw$oDZ^yNG2WA2kBbK-~Nn>#DB zni-*Q)hVCEyTjq2GcKN5@%O5uJ->~pP`Z2NVU=goXtkBp#9bmYposaPJGcA;K=tpRUd}=B$Ifk)aCi~N2v?@ML8+*BI1iIL>rj%M+{R-xC9E{i-A8U zeNw4D1BgyAtpM)plzjT1^U!fg)cNGo*)+Z7nbST20^=mfj0y*nemtEvdWY4vKYq!Z z@a(-5^%r_|z!y*!*>N~soD!EL&W+bJ0T$A7NS}aHQ6zAZ5zR)vRe9rP%GJ;A0tPUT zQ5SFar=Y^OEF||E50EiYN!F0%kG7sscbo+#RuXf~OL$B-2>9)1_hCz`w!@d%#VKI? z+(_*fMc@is5)IH=wL|6Ig5G`I2#0Y19DeLa*|mDXmF#y#Nm;KmFbTgu++5% z`@OxA?FdPn0AA1=)1Y_DBZuD18Ex0*d>NnEp$cMe3yUR7EhTmK>28w!IX6_+)vWtr zbrw3%>cPG!Jw|PQ;lf&SpZ#Ipv`_&U_=F>{vKV~jNzUaCGwA>{P{NJ9u+p*av|J>r zbH;WVmtJYO1KZ4&Cf@4J>|5fv$`8b&WH8>8Uo2X_oY>d)I;#1#Q%KDI-`OSgu!D-vR(xv*#`u>VO|0Yv>bIO(pK zsqX<|6!3TMqj+TveqJ}LM>p$%+nF6A6%Q``r38YiSruO?e-kuBgKEapwz>lgXqgJ^ znK0y5n}bEWpF}Aowb6^6Fn6>-zfvdDlhJ|0I-(&dRW#p8jo@n(h0t~hJbMBz%GbNL zDsw8x$&5)l1MW)%B;9tq(&!y@ww_!L67aO22mV%(QAW+d&Jf(<^|!4Jre!xbce~fB zad2_ro!|8|KqV>{Zo`xQH{*R#?KS&6m_gtu)2mKenSiyeh}Z$8z>oe$tb+zG$OVMq z`+Mb$U9U#lx8km)*3fd7w%7w+&v}rK7hdKUC##jb2!QoLh-j}*mH74OBycZme@v|` zQU;i15cN_JY&tZkD=NS!iFoEGrVW}sxq?el@aYy(c2R+U0Da{!j#e=rU%3}l*Cgm}e4I9_U{AnZ1*LgLJA44=L|I zlotd3zuoJ9Gd7&Rw*`mw(yOWzB6bJb+M$)ah?0+)5}Yt4^GZXtyhSH5NtoH?k z^7}^DaMhS^FKJvP>UX*yR~N{PdNV|$nKf#E?fg+|Dj%uV2!b_8WWYPwwOf;7F|0nQ z9or>ce|6rem!~!Hs~WJ?x7+2b3?eV$l*6KHC2)NO(wOC{Gf)$P8x~0e0pI%BH!rI} zA!M1Z@1Ku#%z{{dPNd9#r=sM2Bb$mmwcVH7_hTPPqQ~afb|)lA`PQt~W%%@ipgr_k zvGYt1b(3^Q+niJsDrxn1>B)e#F15E#>Ks);f_kbS{H$YEl>Gw`pv#hIqbvlHAkm^45s6k6{{bM-_?kC%9}F%?ouzPw@ms1O!SjmC%29 zY0u$N7mo!sB#n^g41%wByB_OGt#was2}MJH5>(8J>s?&h1a+QB@kWYKjsU1eN=h(I zqf-+bYV{QI^Kwf(?$_JP(e8t5t45U`?{8lwGjN1C939@=H9l3qSzUEek)kkcjrBS` z_!k6RemT-a(>6~OFm38FM1O=Ks{KjUzhHpg<#=rX#sBg!#8_I|++7<3sx# zVH%YL_AnNv5^Bm`U<$mIp1SoJ+`!e(1M9Q&mB8%lLL{fwnF{Q3V{xf=CyL>vFHS^c z=2vX$a`BgwpZu&Y`tA}N&h%nv{^hv!7NB8LQ!SAdU+~03zg54!?`XYoLI`v|`^_() zz07!J&?65xAFBWLbMr}b( z%$(~gBByD%lEr@wC}_m++Zte4je;JmCFsrKbv^Q<>v*()51`h&>Ed_ytz{K=c9J9uU!5fhQvU$}#fYZ!`p@>cZ-8orDyevxM+omMq=@)+=h9=U?Ze6+-4`fAv=(x@ z6nyY!?g#N3WRHy-*yLe1(`db*Tj^KfgIwR?#?JRdCuuzY-FqqS3d-t1<0|$ASXYVm z^R{TbTOK9U-(|A{yzb;-41rXP=J#0YG|!N~Fnoer*<{`a-Jfc-aGUm5Azu6`AZwr% zV1g@IN#Niy)-4W6GRhuW2z+3+9WBcxTmEJN&O7!5WM!eTsA#3P@84S);GrK zS0!=Ql?P+Pbt?P=X^I+|W?{$48Y2!`q5BLDKpUA2X#fgF0yWB^r~-IA`nG65QLe>AI2TuS5NY#v(o-!>B2eG?Q>6;Vg=4i&Ym1* zB=7UIjU5pXpwXAG9uhU7%$>k=%-Zh>sz{cRH3QLo{)p1q*Nud`(UpR2vpW2^!9Jejtc2j za72G7nWk3Ea{VIvLY@Zunq}F<8G?r|>}We@+trn-&i`$M%3*x@${^ z>V2sPqg8$^LIC$}t(0Om8X?DHA_~;wia9lJ`fgK&MI_(s;Myl`BuHd#SWhL`XVlgT zTC#t*Y5jYw_Iz*j#c|lLTp`NfpkJyna!H5IgD9T`?0eE{B8A?<8_S=HH<91!!}2IT z$F7TdzeBK(b(viYH^cQ1_r8bypJM`H8KoBawY4TD)Jv+&2x&D^(yOH=M@b2}lw3c)bfk{M+p9qy;(MANI>L*PmLva@uf)<})hKHlWAs7`Nw* zZTUfZADt0Z{NY60A?qnsvP>c{6CUITh}zb`VARfRPi-w2M|yz%6(8QSa(&M+aqF9E zBx!1|8@cm)b{9L!+R;7T?_5H=rhxD<(4K|8?cT;I{ZX@GYP0ayI`29?pgf&^vtt5a zct5nH+@Q#E<%o_w0l2;mw~loy6Umu|^|lHebpkudh>O_5%>U*9s9yE{sh4hVxPCE$ z@|=V2vCQ!eXJRTzQhTn+T^041AR&5&PBzd(tR~5_eyzp4Dh<5_OemL+`1tacx#1!e ze&Ng`Sj?xozRvTseVc-%jo_o<+OJ}`>jGN@JqTiq(5X4TER09>9DvIqNyn6bXFs0^ zQ%b$q1l5q9G6Xp~*6@y{TDLI+HeW8hXRv**PZ%E0`}jd)4E^jtpa9;DmmP-@|DtV{N`)&57SEOm7 zU)hr3fYi*=Nw9cXxInRvAvc?@FPwU{E{Nz)OvwS6PIag}(AHf3E@Zc@cu;lH^!LL) zc>^50rde4!o1zPp2yBvRywZwjw@dTzc%ZS`xEL%5NF;eT!KFtmN&O3Do-HzhUMBw{ zdVPVUo@FTjX%fOOuQj9D-vg|{itS>aE<1(C8oo<2W&5AGZDA%Q;R40eQ{zKd%TRbL zx<>~0Y;dMbwde1)*RoPc>%*2hdK;D;OS38vBvl}%x14KdHEM68+={%m9Lf&9`Ma>G zByt(e*M59`zHt#-`#C`#mEGsh33(_ORWf%K5WU`vkac3=onr&*Bb7_rBZW~Gq_0DN z@(h&y=L_=gl)F@?8>&c$n?vTr5P(ftI= z(!9Z)MA=+r<-QPX#$j4z)rPa(mmG{%^#C?m-H7i6^{Hj|85W_V%E!ldNa&X$|Nu_Ju%(4CHgKWr0nCC zJH&`}Ihp#(cZ2(1fu$ON(sLMoUR%5#Sb;Hd()d)de-S-2@QURyU7}GgfN3=37)Cqn zY2*L*T}}v_X30H+z6Y4_vE`YMmus3ax8FeU1N4?Z$%pjNme-kSRYM;FmoD2Y3E}?*K^(M~bGNRoG%fm0 z4eDS^y<#~`nHgrp?JF0@xh9EJ6>uD<993}$N0RjE%eFXguTY86phd()w|3qmj4$R1 z)qbn~<$BBlLNfeLV+4I*+@Sqj(;Q?$Htwb0e?6_#@*8exnUKx+e9W#loHzX4|Gm%p zUoKQ5lv$~Xz9M$XkxPM@W?xh_lj7tm3c_aV=ZQ;!##gHP;~A!vBERy@v7iq1TMBM5|j9xWWhld%? zd;;MTd->g1hcf*%Et%_SgPDb*VGmXmm23A_6mP1FQU$)}A|Zwf1o<|c-oFpD+RW>V z6B(nFBia0S*~ui59cXtvg+j6N=^OlU?eYzjpDcZtpxUS#gv#>CQF1RoO=R}8S+=2-Cm=BoE zP3Ih+FunX>V{k7&#`e3PTWz(?mn3s8n;B9JM)uoR6M<0Dk{<-5mM{ zgM|!J;y0yeLp`uzuqfe#u0w|+U6hw8W0K%b2unqFsvBqQrBfGqe}TX$9-s@B1e{MU z;6Z#G>5$hno~x8GFQqr3WQ`^sixGJg?;rs&fT6uq)S@tTvQed*ieMsgC={ctqPMTP z_mhGDI}K>?$a*$6#bJ`85A{Qs;VR0u+@@D)D3V|6*`n3^%+hjwL99c4 zUYCZ?mW}iF*k0tY@<VdJPRhwI#XkSS4lpwYSStIhfD)$IR z_DmFu?fO0D$}U7N7*IT5z-W64M@HB#$J_&l_WqBpww=gK^~xC&`c**E23(Nm9!Z7> zXQ|4ND5TG+r;0NSm{CcdsO~nw0eqKPbt5YUbQlvW?=qd={kd96Pe$RL&SvX3Mk;uG zO2BTHLmGeR+*8$4YaAm)xL9u;(kD{VV=y}P_+-r7uN5xnx&s!+zE8_6 zb-%A+jD77~rU%rOv(3t)cPAY3wk`=P{CK#ORzsWKo2Ix+F2{>XVU03TO=&*Uvwq<8 z{=knE#(OS*>{L3_hV**)pLa*mm?kr^^0qCRCCO#%>FRm@^!^r!9SZW|1mP87bTLK5 zkx15bT*4UDW}YM}GACuzFxCB5Ed0q~E(?glJ9!)4WJV@0j=cW+?N2zvB)G=PXG(yE zi8p&uyEIP;sDJ)PH+DzzCdpK&l+mlW&BC+Zk{$)Z_cL17sqGJSl0)($r^V_q+aKFm zqqcDQptr%|9Rv2%>Ho!9D-VTU-9fJ^@%U5R~zZ~69FoM^D$JS&{Q<-j)dQggjvPK&p!3^00_SV5M6 zX8Bf0mW-8;d;Vg8Z(@8bb%38;&Ge>z`q)I*{PTlvB!V~d_RCd8i}{Dh0}?Le)(%<&-P`vwc zg$SZnbk;3-Xtw(FFY>joL~rE{xyl^k78^<$o0AAbHC9?^8d`nVz19{%dQw8Hwzgv4e>+N~d- z$4TFLYnP!c_?Y|%(B4;!<>8g7Ec2fG`aHh0m zHj9SpJSaLi6r#y(gYFNx>yvdum4^TN{ip~;!Maj?7-+egXqqh~#-bP~PDrJvp?YC7 zmQYNj51vr&;1zj;wY{Dh&)-+(rpX34*mR^y{seKvCA((9<)Eo092m|@=-P;}=s5DI z+4Da@CtQq_hbM*))`BedI!L<8mmVLrWV@t}5Z{~o37vdF77l;9C|a9O74F}3mQD|N z{fj#GTd%}pFk@XPYRR=T@V@6dI2K3pB{OclV;Z^mn0T-jp}Nx>gI2W2qTXCJCB)zS z=2LcUF!!(68Y1nTC%JPV1APc@L~_`hQ+K+B`5h2Vo+OQ!5n9*knD>j}m%V^;uvpUb z#5qvSeigAno(}V=vLJw|*UNO~{dS4hrh?QT3%?&9@6z8Scil7;UA20dHa+l1sGCk^ zfp+!-$&uPEcJ6)uMey6I5F5SyO>xpS4dBgy1hpGdy208r%Bw|VVbZ8?_TvlbFxuF! z8;D2lHW0y<(5;x+-E@a%TwIkE3~Bl-)w-pOq8wZ(ISnq}kuR{L*JN`MlPRmeFlb z28DIBw|KK_?!9~4ebr#MWQJcdos=dRH_bk zccXV%M^zvhpojQ^Lihue(QJA4ULWfWgNoZ2s=NaBFu2^enbYB(Qb+5)a@h-KH&}5b%s9anSEqYLjH}*Jgj{X?oKX(%jwb1wcR-C zbgU%H>3;w`FqrDIE@$SRXyUET#DVX}!UY=yjy+->G4geiCr1d`K^}sMm9fr16nK!d zu=JX*0}}8QYLi+YWN-;p^-`Os^?A&$a7{XTnA*|psB_{wk<9ugMolU?iyOE0MDQaV zd$_IYi3}EsGQsQ9k>p7Fn(z96cO}!zjK&?|Ttf3zPo?8TpkqycNst_($8o7gMd4qR z+5QL*u{OT|B#K~}9yV{%U7H7^OYrFCq=yAce0{dGT8T^C<%vI(F{80lX^epKX8Y@D z&nF`KZVU5eJw~^8%@%sPcsYR<>H$l4M0Oz=?&H83s+F}JPO(<;)>aQ7)Zrz!^6gjI zl7AaNPVLq-JKY$wS%Y>nCmVX+N27f!#wykgYlkKWYFIBDDSRFl2{@Z9E_`M1qqQ%^fLGIL!L`w}0G^w{A8zj=&o+rsuytm8 zyO28;$_`^kxA*|7w;rUXzvXzTv}DJ8HZd&~@XR$xET$MxfdG$7RQ>42-iF zFahxl|CgMYAHwaZn}hcBa=R#9gw)(k?>Xga;`yqL1K~KiTF&xcLhNh;4jkI0C*IMo zlES>YZS>IWk3X;by8Xj1DT`SoPk&Z6<0afKkvM^|+6xtbFm;^1!$<%(Y!+6kj{R?i*mElN79&BC-;pcNBxeF`OJruX&TV*rbD;A+$!HAa++bSM zB@$p%cA!5a-$6KGH}dOR=cKH4U&1yY2gP4@$4M3{Y%!PjSEH0jzQV2`0FSx;ev#eR*{$cHbh$#)Wm1ox6ENAvprF1Bn^6+;k>P^Zr zuMw|6xDsr~H)G;lXh2KOz?<%odFHG|*!v|TAA-}t-eP-lbbgqq;$M+LT~|ajC85^4 zwJL>wQp(;q1Z?Wnjx7+C&S0$F))<TQP#y$b?!}kAn>NJFz)y8~+TqLrb9mwY zIvgo+ezK*-x!u*nF@w1r;hcS6jD8&akVJwgJ2&O&^wC$IN$G-;jXxidj(!wB6ICEn zyyjHJChm$DV|W;WeLimN$0~buaQ3mdDs8@RzmjEypnqYA*M;+P0lPYtFLe3)%#^@k^-K_o=UC-ob8lxZQ*O2T|wm3Qq0zRpHlZL{S4 z1vAruL?f0pUVb$B647d}r~u%ZN0ESNYptfq8P>=vih2v)@0t$ojz;>MHE$L| zlZXPJ8{@>#GMD=E?>@!-xa}lwJ_rs6{dmZHQq=dhpKK8RnQmkevw%5$pY{(S#q`cO zy5ibOS0al)=T|*QqMF%j9}Wi*KSEEsq;Aj6hkbafZ(m2nUXQNZ2d!|3*-;oRUj|5E4r zH)z&t!vp8Y=Gg^gCs?ehP>v3rOzp$ILzAL~kv!F@QCC_q`TH_4@bcT8_?{>CCz(c` z)5bcHA4tlUCM+C}G}H-B^|cs^4n zOis2;)TIGxaF=_rH;WbMI(?{B?69`-glde>jzw_6lzYzw3l=ac*b z+MaL^Mq|z6OtlGyB|v8jk90}IO8#NQ$~G`vwrwizwJJa&zkjl~g`=GyQXN7wEy1g| zMJT}A^>0yYWw#+`w2xYPw~dqtmrJiU^HHY1NgkxhQ^wirdWmkjjCo{JDQvtjnEiP{ zgEXKIoFSCD|Emg}3VMb#k2nYFS@-md%~or_`20j7A!h`3SeIg0^H>9#Ahw;>()rtO z2PLWlBgBR4F*bwoxELQUb}E2Z@M6mRYkiG<(ykC zG>PPs{K}fg*_3e$8X&ncMbQC)dn)=eLZRDn>Sfs97z74+r%Ly~cS;gbcPxj4^e&c` zh>K)A%fO7Llc+m7@|` zR4i`ttPWl0nl85-PaT>4P0_dSyx%7k>NLRjLvvc2Dr%=|)5-s|1lbe`w&1KNLQ!$O z5`V1ErA*~}*XRlPg}Q8{rPD)iK2A06dcpQp9jihuSd1wqGs)q#~X#V`-F zL`1_~8e^ChMCEPirGL`N+}dOomSGtJ!b<^rV@XiisMj;7JdOC&g2Q2NYq>e;f74EL zOj>NpbX%*eW=2U3M3pL1%RRB&=G_bj2cOHrLmaO0%Zhmj62vX$gQQ0e z%z{?_(TDj=C%nvRbmZKXl(_cxu~q4w@hrq)GGD@xQMF6T`5Y3*svvG-1(C{6d1v4y z84@-|3-Ys~tmcxuwX_+8qWil1%2MWc|(*lX7d} zdYht=OZ}a`YnSF}Hi^Xdl_piw7kAw(myHpVPxR8?KNE6A{@C+u7WQ9~-Ykyn*^-c; zb_;7&tceJRtcd(HvP9r*;YT#+cCq<%y~*te4NbLLnX=$|)XJ`E`(sROd7f!oAq%xdCmXPv-CX;(mvbaIAgFq_NXHKc7>Z!ol zqQU)JT!U#Ysta2{TqFF8O`NwFFxGEmSL0E*A--&ocRnUwZAW~P_O}RB>6DR7j?5E& zgZ*)gSMA@^Xz4Arziip))L;r>jmg_<(W8@xnkPJkOz1OTXAuny3=9kv1x@+NPoKa2 E4-Uwn`Tzg` literal 0 HcmV?d00001 diff --git a/taiga/base/api/static/api/img/grid.png b/taiga/base/api/static/api/img/grid.png new file mode 100644 index 0000000000000000000000000000000000000000..878c3ed5c196539c4e2da35b7787ab08e98b9cca GIT binary patch literal 1458 zcmeAS@N?(olHy`uVBq!ia0y~yV2EO1U^L=jV_;yYsyN=rz`(#+;1OBOz`!jG!i)^F z=14FwFy>{3M3hAM`dB6B=jtV<`9GBGMIdz2D!M|aoOm@!_toHsXebF z0|WCbPZ!6KiaBrZ9xRd$6>zvH$K1H+!GC)ZGlxUT6N3&O>g+MyXT$$|O|0zGv_Cq& z(H9Mk?a!Zie|Yw>-W>hvsuBi$!O4A3yXqrbor^N= z?)Y6HXq9wWXk%jMGYcyv91ew>D?P1)*W2XyMM1a-4$s}#;F1%u$g-~wpZ`II98tgP zUUR9I7&9yQy^mBAXK0&h5^>>brXwglEu(L-GFsGN_-|uk=Zy_6#p`NH*8Km)_~-KF zoNo=!_8c#sWAa>N-HQ`bo%5fbxey)esBZQ2{nIXNu`Os-bmok|{*PU&?;Wq;K7ViC z-rR_~+KOL=H4)jidxUnsl5uxWInH}pb>|{zg1Kqa^u)65X8UD|ehd)XEf4<}YtNlW|awFoTf4#T=YOyae zyVmG=clY;{ zAzn+2N`B@EsFntb4f8KoFNf?;?#bt7DDRW|qYf%nJzf1=);T3K0RY0WPZ9tC literal 0 HcmV?d00001 diff --git a/taiga/base/api/static/api/js/bootstrap.min.js b/taiga/base/api/static/api/js/bootstrap.min.js new file mode 100644 index 00000000..e0b220f4 --- /dev/null +++ b/taiga/base/api/static/api/js/bootstrap.min.js @@ -0,0 +1,7 @@ +/** +* Bootstrap.js by @fat & @mdo +* plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;a("body").addClass("modal-open"),this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1).focus(),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.trigger("shown")}):b.$element.trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,a("body").removeClass("modal-open"),this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(a){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('