From c34634b39a0b25917825da2656d60dc5462d010f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 29 Apr 2016 15:06:37 +0200 Subject: [PATCH 001/261] Fixing load_dump command --- taiga/export_import/management/commands/load_dump.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index a1d919f0..08f7811b 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -21,8 +21,8 @@ from django.db.models import signals from optparse import make_option from taiga.base.utils import json -from taiga.export_import.import services -from taiga.export_import.exceptions as err +from taiga.export_import import services +from taiga.export_import import exceptions as err from taiga.export_import.renderers import ExportRenderer from taiga.projects.models import Project from taiga.users.models import User From 66d5c372b275142cb8ca214889c83100c59a9d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Sat, 30 Apr 2016 16:23:58 +0200 Subject: [PATCH 002/261] Improve projects admin panel --- taiga/projects/admin.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 18bca9c5..44a2b412 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -16,6 +16,9 @@ # along with this program. If not, see . from django.contrib import admin +from django.core.urlresolvers import reverse +from django.utils.html import format_html +from django.utils.translation import ugettext_lazy as _ from taiga.projects.milestones.admin import MilestoneInline from taiga.projects.notifications.admin import NotifyPolicyInline @@ -67,18 +70,25 @@ class MembershipInline(admin.TabularInline): class ProjectAdmin(admin.ModelAdmin): list_display = ["id", "name", "slug", "is_private", - "is_featured", "is_looking_for_people", - "owner", "created_date"] - + "owner_url", "blocked_code", "is_featured"] list_display_links = ["id", "name", "slug"] - list_filter = ("is_private", "is_featured", "is_looking_for_people") - list_editable = ["is_featured"] + list_filter = ("is_private", "blocked_code", "is_featured") + list_editable = ["is_featured", "blocked_code"] search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"] inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline] # NOTE: TextArrayField with a choices is broken in the admin panel. exclude = ("anon_permissions", "public_permissions") + def owner_url(self, obj): + if obj.owner: + url = reverse('admin:{0}_{1}_change'.format(obj.owner._meta.app_label, + obj.owner._meta.model_name), + args=(obj.owner.pk,)) + return format_html("{user}", url=url, user=obj.owner) + return "" + owner_url.short_description = _('owner') + def get_object(self, *args, **kwargs): self.obj = super().get_object(*args, **kwargs) return self.obj From 8cd6c280106c777bb9c709d259e45b66c8172a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Sun, 1 May 2016 19:10:28 +0200 Subject: [PATCH 003/261] [i18n] Update locales --- taiga/locale/ca/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/de/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/en/LC_MESSAGES/django.po | 190 +++++++++------ taiga/locale/es/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/fi/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/fr/LC_MESSAGES/django.po | 271 +++++++++++++-------- taiga/locale/it/LC_MESSAGES/django.po | 194 +++++++++------ taiga/locale/nl/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/pl/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/pt_BR/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/ru/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/sv/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/tr/LC_MESSAGES/django.po | 192 +++++++++------ taiga/locale/zh-Hant/LC_MESSAGES/django.po | 192 +++++++++------ 14 files changed, 1641 insertions(+), 1126 deletions(-) diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po index 9deffb7e..d643b4f8 100644 --- a/taiga/locale/ca/LC_MESSAGES/django.po +++ b/taiga/locale/ca/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ca/)\n" @@ -188,7 +188,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -349,7 +349,7 @@ msgid "Error in filter params types." msgstr "" #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "" @@ -480,62 +480,6 @@ msgstr "Es necessita arxiu dump." msgid "Invalid dump format" msgstr "Format d'arxiu dump invàlid" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "" @@ -555,14 +499,103 @@ msgstr "Conté camps personalitzats invàlids." msgid "Name duplicated for the project" msgstr "" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1148,6 +1181,15 @@ msgstr "Administrar valors de projecte" msgid "Admin roles" msgstr "Administrar rols" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "Amo" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Arguments incomplets." @@ -1194,14 +1236,6 @@ msgstr "" msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "Amo" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2359,39 +2393,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Opcions per defecte" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Estatus d'històries d'usuari" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Punts" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Estatus de tasques" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Estatus d'incidéncies" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Tipus d'incidéncies" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Prioritats" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Severitats" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Rols" diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po index d38d46fd..7b75e1f9 100644 --- a/taiga/locale/de/LC_MESSAGES/django.po +++ b/taiga/locale/de/LC_MESSAGES/django.po @@ -17,8 +17,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/de/)\n" @@ -218,7 +218,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -381,7 +381,7 @@ msgid "Error in filter params types." msgstr "Fehler in Filter Parameter Typen." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' muss ein Integer-Wert sein." @@ -535,62 +535,6 @@ msgstr "Exportdatei erforderlich" msgid "Invalid dump format" msgstr "Ungültiges Exportdatei Format" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "Fehler beim Importieren der Projektdaten" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "Fehler beim Importieren der Listen von Projektattributen" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte " - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "Fehler beim Importieren der Kundenattribute" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "Fehler beim Importieren der Rollen" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "Fehler beim Importieren der Mitgliedschaften" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "Fehler beim Import der Sprints" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "Fehler beim Importieren von Wiki Seiten" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "Fehler beim Importieren von Wiki Links" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "Fehler beim Importieren der Tickets" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "Fehler beim Importieren der User-Stories" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "Fehler beim Importieren der Aufgaben" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "Fehler beim Importieren der Schlagworte" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "Fehler beim Importieren der Chroniken" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden" @@ -610,14 +554,103 @@ msgstr "Enthält ungültige Benutzerfelder." msgid "Name duplicated for the project" msgstr "Der Name für das Projekt ist doppelt vergeben" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "Fehler beim Importieren der Projektdaten" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "Fehler beim Importieren der Rollen" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "Fehler beim Importieren der Mitgliedschaften" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "Fehler beim Importieren der Listen von Projektattributen" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte " + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "Fehler beim Importieren der Kundenattribute" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "Fehler beim Import der Sprints" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "Fehler beim Importieren der User-Stories" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "Fehler beim Importieren der Aufgaben" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "Fehler beim Importieren der Tickets" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "Fehler beim Importieren von Wiki Seiten" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "Fehler beim Importieren von Wiki Links" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "Fehler beim Importieren der Schlagworte" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "Fehler beim Importieren der Chroniken" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Fehler beim Erzeugen der Projekt Export-Datei " -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Fehler beim Laden von Projekt Export-Datei" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1338,6 +1371,15 @@ msgstr "Administrator Projekt Werte" msgid "Admin roles" msgstr "Administrator-Rollen" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "Besitzer" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Unvollständige Argumente" @@ -1384,14 +1426,6 @@ msgstr "Teil-Aktualisierungen sind nicht unterstützt" msgid "Project ID not matches between object and project" msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "Besitzer" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2831,39 +2865,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Voreingestellte Optionen" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Status für User-Stories" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Punkte" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Aufgaben Status" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Ticket Status" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Ticket Arten" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Prioritäten" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Gewichtung" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Rollen" diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index 866f8483..4cde7d23 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: 2016-04-19 16:00+0200\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -180,7 +180,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -341,7 +341,7 @@ msgid "Error in filter params types." msgstr "" #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "" @@ -469,62 +469,6 @@ msgstr "" msgid "Invalid dump format" msgstr "" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "" @@ -544,14 +488,103 @@ msgstr "" msgid "Name duplicated for the project" msgstr "" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1121,6 +1154,15 @@ msgstr "" msgid "Admin roles" msgstr "" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "" @@ -1167,14 +1209,6 @@ msgstr "" msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2326,39 +2360,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "" diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index 7538d688..ec287179 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -16,8 +16,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/es/)\n" @@ -205,7 +205,7 @@ msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada. #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "Elemento bloqueado" @@ -369,7 +369,7 @@ msgid "Error in filter params types." msgstr "Error en los típos de parámetros de filtrado" #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' debe ser un valor entero." @@ -522,62 +522,6 @@ msgstr "Se necesita el fichero con los datos exportados" msgid "Invalid dump format" msgstr "Formato de fichero de exportación inválido" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "error importando los datos del proyecto" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "error importando la listados de valores de attributos del proyecto" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "error importando los valores por defecto de los atributos del proyecto" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "error importando los atributos personalizados" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "error importando los roles" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "error importando los miembros" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "error importando los sprints" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "error importando las páginas del wiki" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "error importando los enlaces del wiki" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "error importando las peticiones" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "error importando las historias de usuario" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "error importando las tareas" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "error importando las etiquetas" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "error importando los timelines" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" no se ha encontrado en este proyecto" @@ -597,14 +541,103 @@ msgstr "Contiene attributos personalizados inválidos." msgid "Name duplicated for the project" msgstr "Nombre duplicado para el proyecto" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "error importando los datos del proyecto" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "error importando los roles" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "error importando los miembros" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "error importando la listados de valores de attributos del proyecto" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "error importando los valores por defecto de los atributos del proyecto" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "error importando los atributos personalizados" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "error importando los sprints" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "error importando las historias de usuario" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "error importando las tareas" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "error importando las peticiones" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "error importando las páginas del wiki" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "error importando los enlaces del wiki" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "error importando las etiquetas" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "error importando los timelines" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Erro generando el volcado de datos del proyecto" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Error cargando el volcado de datos del proyecto" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1318,6 +1351,15 @@ msgstr "Administrar valores de proyecto" msgid "Admin roles" msgstr "Administrar roles" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "Dueño" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Argumentos incompletos" @@ -1366,14 +1408,6 @@ msgstr "La actualización parcial no está soportada." msgid "Project ID not matches between object and project" msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "Dueño" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2766,39 +2800,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Opciones por defecto" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Estados de historia de usuario" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Puntos" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Estado de tareas" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Estados de peticion" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Tipos de petición" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Prioridades" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Gravedades" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Roles" diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po index 42a4be30..4034a724 100644 --- a/taiga/locale/fi/LC_MESSAGES/django.po +++ b/taiga/locale/fi/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fi/)\n" @@ -190,7 +190,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -351,7 +351,7 @@ msgid "Error in filter params types." msgstr "Error in filter params types." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' must be an integer value." @@ -505,62 +505,6 @@ msgstr "Tarvitaan tiedosto" msgid "Invalid dump format" msgstr "Virheellinen tiedostomuoto" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "virhe projektidatan tuonnissa" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "virhe atribuuttilistan tuonnissa" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "virhe oletusarvojen tuonnissa" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "virhe omien arvojen tuonnissa" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "virhe roolien tuonnissa" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "virhe jäsenyyksien tuonnissa" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "virhe kierroksien tuonnissa" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "virhe wiki-sivujen tuonnissa" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "virhe viki-linkkien tuonnissa" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "virhe pyyntöjen tuonnissa" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "virhe käyttäjätarinoiden tuonnissa" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "virhe tehtävien tuonnissa" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "virhe avainsanojen sisäänlukemisessa" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "virhe aikajanojen tuonnissa" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" ei löytynyt tästä projektista" @@ -580,14 +524,103 @@ msgstr "Sisältää vieheellisiä omia kenttiä." msgid "Name duplicated for the project" msgstr "Nimi on tuplana projektille" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "virhe projektidatan tuonnissa" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "virhe roolien tuonnissa" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "virhe jäsenyyksien tuonnissa" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "virhe atribuuttilistan tuonnissa" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "virhe oletusarvojen tuonnissa" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "virhe omien arvojen tuonnissa" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "virhe kierroksien tuonnissa" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "virhe käyttäjätarinoiden tuonnissa" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "virhe tehtävien tuonnissa" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "virhe pyyntöjen tuonnissa" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "virhe wiki-sivujen tuonnissa" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "virhe viki-linkkien tuonnissa" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "virhe avainsanojen sisäänlukemisessa" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "virhe aikajanojen tuonnissa" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Virhe tiedoston luonnissa" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Virhe tiedoston latauksessa" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1277,6 +1310,15 @@ msgstr "Hallinnoi projektin arvoja" msgid "Admin roles" msgstr "Hallinnoi rooleja" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "omistaja" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Puutteelliset argumentit" @@ -1323,14 +1365,6 @@ msgstr "" msgid "Project ID not matches between object and project" msgstr "Projekti ID ei vastaa kohdetta ja projektia" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "omistaja" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2728,39 +2762,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Oletusoptiot" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Käyttäjätarinatilat" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Pisteet" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Tehtävien tilat" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Pyyntöjen tilat" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "pyyntötyypit" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Kiireellisyydet" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Vakavuudet" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Roolit" diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po index f84c878e..61fa72db 100644 --- a/taiga/locale/fr/LC_MESSAGES/django.po +++ b/taiga/locale/fr/LC_MESSAGES/django.po @@ -17,13 +17,14 @@ # Regis TEDONE , 2015 # Sébastien Talbot , 2016 # Stéphane Mor , 2015 +# Thierno Rignoux , 2016 # William Godin , 2015 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fr/)\n" @@ -214,7 +215,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "Élément bloqué" @@ -372,14 +373,14 @@ msgstr "Erreur de précondition" #: taiga/base/exceptions.py:217 msgid "No room left for more projects." -msgstr "" +msgstr "Limite de projets atteinte." #: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." msgstr "Erreur dans les types de paramètres de filtres" #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' doit être une valeur entière." @@ -539,63 +540,6 @@ msgstr "Fichier de dump obligatoire" msgid "Invalid dump format" msgstr "Format de dump invalide" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "Erreur lors de l'importation de données" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "erreur lors de l'importation des listes des attributs de projet" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "" -"erreur lors de l'importation des valeurs par défaut des attributs de projet" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "Erreur à l'importation des champs personnalisés" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "Erreur à l'importation des rôles" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "Erreur à l'importation des groupes d'utilisateurs" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "Erreur lors de l'importation des sprints." - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "Erreur à l'importation des pages Wiki" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "Erreur à l'importation des liens Wiki" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "erreur à l'importation des problèmes" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "erreur à l'importation des histoires utilisateur" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "Erreur lors de l'importation des tâches." - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "erreur lors de l'importation des mots-clés" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "erreur lors de l'import des timelines" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" non trouvé dans the projet" @@ -615,14 +559,104 @@ msgstr "Contient des champs personnalisés non valides." msgid "Name duplicated for the project" msgstr "Nom dupliqué pour ce projet" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "Erreur lors de l'importation de données" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "Erreur à l'importation des rôles" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "Erreur à l'importation des groupes d'utilisateurs" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "erreur lors de l'importation des listes des attributs de projet" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "" +"erreur lors de l'importation des valeurs par défaut des attributs de projet" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "Erreur à l'importation des champs personnalisés" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "Erreur lors de l'importation des sprints." + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "erreur à l'importation des histoires utilisateur" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "Erreur lors de l'importation des tâches." + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "erreur à l'importation des problèmes" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "Erreur à l'importation des pages Wiki" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "Erreur à l'importation des liens Wiki" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "erreur lors de l'importation des mots-clés" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "erreur lors de l'import des timelines" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Erreur dans la génération du dump du projet" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Erreur au chargement du dump du projet" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -838,6 +872,17 @@ msgid "" "---\n" "The Taiga Team\n" msgstr "" +"\n" +"Hey %(user)s,\n" +"\n" +"Votre dump a été correctement importé.\n" +"\n" +"Vous pouvez voir le(s) %(project)s ici :\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"L'équipe Taiga\n" #: taiga/export_import/templates/emails/load_dump-subject.jinja:1 #, python-format @@ -1290,6 +1335,15 @@ msgstr "Administrer les paramètres du projet" msgid "Admin roles" msgstr "Administrer les rôles" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "propriétaire" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "arguments manquants" @@ -1316,13 +1370,15 @@ msgstr "L'utilisateur n'existe pas" #: taiga/projects/api.py:366 msgid "The user must be already a project member" -msgstr "" +msgstr "L'utilisateur doit déjà être un membre du projet" #: taiga/projects/api.py:672 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" +"Le projet doit avoir un propriétaire et au moins l'un de ses membres doit " +"être un administrateur actif." #: taiga/projects/api.py:706 msgid "You don't have permisions to see that." @@ -1336,14 +1392,6 @@ msgstr "Mises à jour partielles non supportées" msgid "Project ID not matches between object and project" msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "propriétaire" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -1415,15 +1463,15 @@ msgstr "Talky" #: taiga/projects/choices.py:32 msgid "This project is blocked due to payment failure" -msgstr "" +msgstr "Ce projet a été bloqué pour cause d'impayé" #: taiga/projects/choices.py:33 msgid "This project is blocked by admin staff" -msgstr "" +msgstr "Ce projet a été bloqué par l'équipe administrative" #: taiga/projects/choices.py:34 msgid "This project is blocked because the owner left" -msgstr "" +msgstr "Ce projet est bloqué car son propriétaire est parti" #: taiga/projects/custom_attributes/choices.py:27 msgid "Text" @@ -1842,7 +1890,7 @@ msgstr "couleurs des tags" #: taiga/projects/models.py:221 msgid "project transfer token" -msgstr "" +msgstr "jeton de transfert de projet" #: taiga/projects/models.py:225 msgid "blocked code" @@ -2508,6 +2556,8 @@ msgstr "version" msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" +"Vous ne pouvez pas quitter le projet si vous en êtes le propriétaire ou " +"qu'il n'y a pas d'autre administrateur." #: taiga/projects/serializers.py:172 msgid "Email address is already taken" @@ -2519,77 +2569,78 @@ msgstr "Rôle non valide pour le projet" #: taiga/projects/serializers.py:195 msgid "The project owner must be admin." -msgstr "" +msgstr "Le propriétaire du projet doit être un administrateur." #: taiga/projects/serializers.py:198 msgid "At least one user must be an active admin for this project." msgstr "" +"Au moins un utilisateur doit être un administrateur actif de ce projet." -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Options par défaut" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Etats de la User Story" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Points" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Etats des tâches" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Statuts des problèmes" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Types de problèmes" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Priorités" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Sévérités" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Rôles" #: taiga/projects/services/members.py:116 msgid "You have reached your current limit of memberships for private projects" -msgstr "" +msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets privés" #: taiga/projects/services/members.py:120 msgid "You have reached your current limit of memberships for public projects" -msgstr "" +msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets publics" #: taiga/projects/services/projects.py:69 #: taiga/projects/services/projects.py:106 taiga/users/services.py:582 msgid "You can't have more private projects" -msgstr "" +msgstr "Vous avez atteint le nombre maximum de projets privés" #: taiga/projects/services/projects.py:73 #: taiga/projects/services/projects.py:110 taiga/users/services.py:585 msgid "" "This project reaches your current limit of memberships for private projects" -msgstr "" +msgstr "Ce projet privé est le dernier que vous pouvez rejoindre" #: taiga/projects/services/projects.py:77 #: taiga/projects/services/projects.py:114 taiga/users/services.py:589 msgid "You can't have more public projects" -msgstr "" +msgstr "Vous avez atteint le nombre maximum de projets publics." #: taiga/projects/services/projects.py:81 #: taiga/projects/services/projects.py:118 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for public projects" -msgstr "" +msgstr "Ce projet public est le dernier que vous pouvez rejoindre" #: taiga/projects/services/stats.py:196 msgid "Future sprint" @@ -2608,7 +2659,7 @@ msgstr "Jeton invalide" #: taiga/projects/services/transfer.py:66 msgid "Token has expired" -msgstr "" +msgstr "Le jeton est périmé" #: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." @@ -2653,6 +2704,11 @@ msgid "" "Management Tool.

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

Vous avez été invité à Taiga !

\n" +"

Hey ! %(full_name)s vous a invité à rejoindre le projet %(project)s.
Taiga est un outil de gestion de projet Agile libre et open source." +"

" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:17 #, python-format @@ -2784,7 +2840,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 #, python-format msgid "

%(new_owner_name)s says:

" -msgstr "" +msgstr "

%(new_owner_name)s a dit :

" #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 msgid "" @@ -2793,6 +2849,10 @@ msgid "" "p>\n" " " msgstr "" +"\n" +"

À partir de maintenant, votre nouveau status pour ce projet sera celui " +"d'Administrateur.

\n" +" " #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 #, python-format @@ -2813,6 +2873,9 @@ msgid "" "\n" "From now on, your new status for this project will be \"admin\".\n" msgstr "" +"\n" +"À partir de maintenant, votre nouveau status pour ce projet sera celui " +"d'Administrateur.\n" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 @@ -2822,6 +2885,8 @@ msgid "" "\n" "The Taiga Team\n" msgstr "" +"\n" +"L'Équipe Taiga\n" #: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 #, python-format @@ -2847,6 +2912,8 @@ msgid "" "

%(rejecter_name)s says:

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

%(rejecter_name)s a dit :

" #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 msgid "" @@ -2855,6 +2922,10 @@ msgid "" "different person.

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

Vous pouvez toujours, si vous le désirez, essayer de transférer la " +"propriété du projet à quelqu'un d'autre.

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 @@ -2914,7 +2985,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 #: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 msgid "Continue" -msgstr "" +msgstr "Continuer" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 #, python-format @@ -2960,6 +3031,8 @@ msgid "" "

%(owner_name)s says:

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

%(owner_name)s a dit:

" #: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 msgid "" @@ -3344,7 +3417,7 @@ msgstr "" #: taiga/users/admin.py:49 msgid "id" -msgstr "" +msgstr "id" #: taiga/users/admin.py:81 msgid "Project Ownership" @@ -3364,7 +3437,7 @@ msgstr "Permissions" #: taiga/users/admin.py:123 msgid "Restrictions" -msgstr "" +msgstr "Restrictions" #: taiga/users/admin.py:125 msgid "Important dates" @@ -3674,7 +3747,7 @@ msgstr "" "\n" "Merci pour votre inscription sur Taiga\n" "\n" -"Nous espérons que vous l'appréciez\n" +"Nous espérons que vous l'apprécierez\n" "\n" "Nous avons construit Taiga car nous voulions que l'outil de gestion de " "projet que nous utilisons au quotidien, nous rappelle en permanence pourquoi " diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po index c1f4fcd6..8090f674 100644 --- a/taiga/locale/it/LC_MESSAGES/django.po +++ b/taiga/locale/it/LC_MESSAGES/django.po @@ -15,8 +15,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/it/)\n" @@ -201,7 +201,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -365,7 +365,7 @@ msgid "Error in filter params types." msgstr "Errore nel filtro del tipo di parametri." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'Progetto' deve essere un valore intero." @@ -531,63 +531,6 @@ msgstr "E' richiesto un file di dump" msgid "Invalid dump format" msgstr "Formato di dump invalido" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "Errore nell'importazione del progetto dati" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "Errore nell'importazione della lista degli attributi di progetto" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "" -"Errore nell'importazione dei valori predefiniti degli attributi del progetto." - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "Errore nell'importazione degli attributi personalizzati" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "Errore nell'importazione i ruoli" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "Errore nell'importazione delle iscrizioni" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "errore nell'importazione degli sprints" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "Errore nell'importazione delle pagine wiki" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "Errore nell'importazione dei link di wiki" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "errore nell'importazione dei problemi" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "Errore nell'importazione delle user story" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "Errore nell'importazione dei compiti " - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "Errore nell'importazione dei tags" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "Errore nell'importazione delle timelines" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" non è stato trovato in questo progetto" @@ -607,14 +550,104 @@ msgstr "Contiene campi personalizzati invalidi." msgid "Name duplicated for the project" msgstr "Il nome del progetto è duplicato" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "Errore nell'importazione del progetto dati" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "Errore nell'importazione i ruoli" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "Errore nell'importazione delle iscrizioni" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "Errore nell'importazione della lista degli attributi di progetto" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "" +"Errore nell'importazione dei valori predefiniti degli attributi del progetto." + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "Errore nell'importazione degli attributi personalizzati" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "errore nell'importazione degli sprints" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "Errore nell'importazione delle user story" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "Errore nell'importazione dei compiti " + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "errore nell'importazione dei problemi" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "Errore nell'importazione delle pagine wiki" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "Errore nell'importazione dei link di wiki" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "Errore nell'importazione dei tags" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "Errore nell'importazione delle timelines" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Errore nella creazione del dump di progetto" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Errore nel caricamento del dump di progetto" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1413,6 +1446,15 @@ msgstr "Valori dell'amministratore del progetto" msgid "Admin roles" msgstr "Ruoli dell'amministratore" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "proprietario" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Argomento non valido" @@ -1459,14 +1501,6 @@ msgstr "Aggiornamento non parziale non supportato" msgid "Project ID not matches between object and project" msgstr "L'ID di progetto non corrisponde tra oggetto e progetto" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "proprietario" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -3005,39 +3039,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Opzioni predefinite" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Stati della storia utente" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Punti" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Stati del compito" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Stati del problema" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Tipologie del problema" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Priorità" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Criticità" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Ruoli" diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po index d1782718..e0d6070d 100644 --- a/taiga/locale/nl/LC_MESSAGES/django.po +++ b/taiga/locale/nl/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/nl/)\n" @@ -199,7 +199,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -361,7 +361,7 @@ msgid "Error in filter params types." msgstr "Fout in filter params types." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' moet een integer waarde zijn." @@ -518,62 +518,6 @@ msgstr "Dump file nodig" msgid "Invalid dump format" msgstr "Ongeldig dump formaat" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "fout bij het importeren van project data" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "fout bij importeren van project attributenlijst" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "fout bij importeren van standaard projectattributen waarden" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "fout bij importeren eigen attributen" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "fout bij importeren rollen" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "fout bij importeren lidmaatschappen" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "fout bij importeren sprints" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "fout bij importeren wiki pagina's" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "fout bij importeren wiki links" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "fout bij importeren issues" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "fout bij importeren user stories" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "fout bij importeren taken" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "fout bij importeren tags" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "fout bij importeren tijdlijnen" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" niet gevonden in dit project" @@ -593,14 +537,103 @@ msgstr "Het bevat ongeldige eigen velden:" msgid "Name duplicated for the project" msgstr "Naam gedupliceerd voor het project" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "fout bij het importeren van project data" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "fout bij importeren rollen" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "fout bij importeren lidmaatschappen" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "fout bij importeren van project attributenlijst" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "fout bij importeren van standaard projectattributen waarden" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "fout bij importeren eigen attributen" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "fout bij importeren sprints" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "fout bij importeren user stories" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "fout bij importeren taken" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "fout bij importeren issues" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "fout bij importeren wiki pagina's" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "fout bij importeren wiki links" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "fout bij importeren tags" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "fout bij importeren tijdlijnen" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Fout bij genereren project dump" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Fout bij laden project dump" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1211,6 +1244,15 @@ msgstr "Admin project waarden" msgid "Admin roles" msgstr "Admin rollen" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "eigenaar" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Onvolledige argumenten" @@ -1257,14 +1299,6 @@ msgstr "" msgid "Project ID not matches between object and project" msgstr "Project ID van object is niet gelijk aan die van het project" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "eigenaar" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2448,39 +2482,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Standaard opties" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Status van User story" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Punten" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Statussen van taken" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Statussen van Issues" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Types van issue" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Prioriteiten" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Ernstniveaus" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Rollen" diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po index 5ba1251c..f7ce189e 100644 --- a/taiga/locale/pl/LC_MESSAGES/django.po +++ b/taiga/locale/pl/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/pl/)\n" @@ -193,7 +193,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -356,7 +356,7 @@ msgid "Error in filter params types." msgstr "Błąd w parametrach typów filtrów." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' musi być wartością typu int." @@ -519,62 +519,6 @@ msgstr "Wymagany plik zrzutu" msgid "Invalid dump format" msgstr "Nieprawidłowy format zrzutu" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "błąd w trakcie importu danych projektu" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "błąd w trakcie importu atrybutów projektu" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "błąd w trakcie importu domyślnych atrybutów projektu" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "błąd w trakcie importu niestandardowych atrybutów" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "błąd w trakcie importu ról" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "błąd w trakcie importu członkostw" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "błąd w trakcie importu sprintów" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "błąd w trakcie importu stron Wiki" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "błąd w trakcie importu linków Wiki" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "błąd w trakcie importu zgłoszeń" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "błąd w trakcie importu historyjek użytkownika" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "błąd w trakcie importu zadań" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "błąd w trakcie importu tagów" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "błąd w trakcie importu osi czasu" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" nie odnaleziono w projekcie" @@ -594,14 +538,103 @@ msgstr "Zawiera niewłaściwe pola niestandardowe." msgid "Name duplicated for the project" msgstr "Nazwa projektu zduplikowana" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "błąd w trakcie importu danych projektu" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "błąd w trakcie importu ról" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "błąd w trakcie importu członkostw" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "błąd w trakcie importu atrybutów projektu" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "błąd w trakcie importu domyślnych atrybutów projektu" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "błąd w trakcie importu niestandardowych atrybutów" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "błąd w trakcie importu sprintów" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "błąd w trakcie importu historyjek użytkownika" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "błąd w trakcie importu zadań" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "błąd w trakcie importu zgłoszeń" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "błąd w trakcie importu stron Wiki" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "błąd w trakcie importu linków Wiki" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "błąd w trakcie importu tagów" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "błąd w trakcie importu osi czasu" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Błąd w trakcie generowania zrzutu projektu" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Błąd w trakcie wczytywania zrzutu projektu" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1319,6 +1352,15 @@ msgstr "Administruj wartościami projektu" msgid "Admin roles" msgstr "Administruj rolami" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "właściciel" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Pola niekompletne" @@ -1365,14 +1407,6 @@ msgstr "" msgid "Project ID not matches between object and project" msgstr "ID nie pasuje pomiędzy obiektem a projektem" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "właściciel" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2785,39 +2819,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Domyślne opcje" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Statusy historyjek użytkownika" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Punkty" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Statusy zadań" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Statusy zgłoszeń" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Typu zgłoszeń" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Priorytety" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Ważność" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Role" diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po index 9b3d2b72..2e440979 100644 --- a/taiga/locale/pt_BR/LC_MESSAGES/django.po +++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po @@ -19,8 +19,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/pt_BR/)\n" @@ -200,7 +200,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -363,7 +363,7 @@ msgid "Error in filter params types." msgstr "Erro nos tipos de parâmetros do filtro." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'projeto' deve ser um valor inteiro." @@ -527,62 +527,6 @@ msgstr "Necessário de arquivo de restauração" msgid "Invalid dump format" msgstr "Formato de aquivo de restauração inválido" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "erro ao importar informações de projeto" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "erro importando lista de atributos do projeto" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "erro importando valores de atributos do projeto padrão" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "erro importando atributos personalizados" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "erro importando funcões" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "erro importando filiações" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "erro importando sprints" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "erro importando páginas wiki" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "erro importando wiki links" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "erro importando casos" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "erro importando user stories" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "erro importando tarefas" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "erro importando tags" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "erro importando linha do tempo" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" não encontrado nesse projeto" @@ -602,14 +546,103 @@ msgstr "Contém campos personalizados inválidos" msgid "Name duplicated for the project" msgstr "Nome duplicado para o projeto" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "erro ao importar informações de projeto" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "erro importando funcões" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "erro importando filiações" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "erro importando lista de atributos do projeto" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "erro importando valores de atributos do projeto padrão" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "erro importando atributos personalizados" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "erro importando sprints" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "erro importando user stories" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "erro importando tarefas" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "erro importando casos" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "erro importando páginas wiki" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "erro importando wiki links" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "erro importando tags" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "erro importando linha do tempo" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Erro gerando arquivo de restauração do projeto" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Erro carregando arquivo de restauração do projeto" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1325,6 +1358,15 @@ msgstr "Valores projeto admin" msgid "Admin roles" msgstr "Funções Admin" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "dono" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Argumentos incompletos" @@ -1371,14 +1413,6 @@ msgstr "Atualizações parciais não são suportadas" msgid "Project ID not matches between object and project" msgstr "ID do projeto não combina entre objeto e projeto" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "dono" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2769,39 +2803,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Opções padrão" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Status de user story" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Pontos" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Status de tarefas" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Status de casos" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Tipos de casos" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Prioridades" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Severidades" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Funções" diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po index f9c67e5f..366ee566 100644 --- a/taiga/locale/ru/LC_MESSAGES/django.po +++ b/taiga/locale/ru/LC_MESSAGES/django.po @@ -15,8 +15,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ru/)\n" @@ -203,7 +203,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -365,7 +365,7 @@ msgid "Error in filter params types." msgstr "Ошибка в типах фильтров для параметров." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' должно быть целым значением." @@ -529,62 +529,6 @@ msgstr "Необходим дамп-файл" msgid "Invalid dump format" msgstr "Неправильный формат дампа" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "ошибка при импорте данных проекта" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "ошибка при импорте списков свойств проекта" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "ошибка при импорте значений по умолчанию свойств проекта" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "ошибка при импорте пользовательских свойств" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "ошибка при импорте ролей" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "ошибка при импорте членства" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "ошибка при импорте спринтов" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "ошибка при импорте вики-страниц" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "ошибка при импорте вики-ссылок" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "ошибка при импорте запросов" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "ошибка импорта историй от пользователей" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "ошибка импорта задач" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "ошибка импорта тэгов" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "ошибка импорта хронологии проекта" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" не найдено в этом проекте" @@ -604,14 +548,103 @@ msgstr "Содержит неверные специальные поля" msgid "Name duplicated for the project" msgstr "Уже есть такое имя для проекта" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "ошибка при импорте данных проекта" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "ошибка при импорте ролей" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "ошибка при импорте членства" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "ошибка при импорте списков свойств проекта" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "ошибка при импорте значений по умолчанию свойств проекта" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "ошибка при импорте пользовательских свойств" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "ошибка при импорте спринтов" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "ошибка импорта историй от пользователей" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "ошибка импорта задач" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "ошибка при импорте запросов" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "ошибка при импорте вики-страниц" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "ошибка при импорте вики-ссылок" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "ошибка импорта тэгов" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "ошибка импорта хронологии проекта" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Ошибка создания свалочного файла для проекта" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Ошибка загрузки свалочного файла проекта" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1326,6 +1359,15 @@ msgstr "Управлять значениями проекта" msgid "Admin roles" msgstr "Управлять ролями" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "владелец" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Список аргументов неполон" @@ -1372,14 +1414,6 @@ msgstr "Частичные обновления не поддерживаютс msgid "Project ID not matches between object and project" msgstr "Идентификатор проекта не подходит к этому объекту" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "владелец" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2784,39 +2818,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Параметры по умолчанию" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Статусу пользовательских историй" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Очки" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Статусы задачи" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Статусы запроса" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Типы запроса" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Приоритеты" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Степени важности" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Роли" diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po index 88143ba9..705d5cc6 100644 --- a/taiga/locale/sv/LC_MESSAGES/django.po +++ b/taiga/locale/sv/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/sv/)\n" @@ -192,7 +192,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -355,7 +355,7 @@ msgid "Error in filter params types." msgstr "Fel i filterparametertyper." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'Projektet\" måste vara ett heltal." @@ -503,62 +503,6 @@ msgstr "Behöver en hämtningsfil" msgid "Invalid dump format" msgstr "Invalid hämtningsfilformat" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "fel vid import av projektdata" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "fel vid import av en lista på projektegenskaper" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "fel vid import av standard projektegenskapsvärden" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "fel vid import av anpassade egenskaper" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "fel vid importering av roller" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "fel vid import av medlemskap" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "felaktig import av sprintar" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "vel vid import av wiki-sidor" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "fel vid import av wiki-länkar" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "fel vid import av ärenden" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "fel vid import av användarhistorier" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "fel vid import av uppgifter" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "fel vid importering av taggar" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "fel vid importering av tidslinje" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" gick inte att hitta för det här projektet" @@ -578,14 +522,103 @@ msgstr "Innehåller felaktigt anpassad fält." msgid "Name duplicated for the project" msgstr "Namnet är upprepad för projektet" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "fel vid import av projektdata" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "fel vid importering av roller" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "fel vid import av medlemskap" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "fel vid import av en lista på projektegenskaper" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "fel vid import av standard projektegenskapsvärden" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "fel vid import av anpassade egenskaper" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "felaktig import av sprintar" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "fel vid import av användarhistorier" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "fel vid import av uppgifter" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "fel vid import av ärenden" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "vel vid import av wiki-sidor" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "fel vid import av wiki-länkar" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "fel vid importering av taggar" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "fel vid importering av tidslinje" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Fel vid skapandet av projektkopia" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Feil vid hämtning av projektkopia" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1166,6 +1199,15 @@ msgstr "Administrera projektvärden" msgid "Admin roles" msgstr "Administratorroller" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "ägare" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Felaktiga argument" @@ -1212,14 +1254,6 @@ msgstr "Delvisa uppdateringar stöds inte. " msgid "Project ID not matches between object and project" msgstr "Projekt-ID stämmer inte mellan objekt och projekt" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "ägare" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2371,39 +2405,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Standardval" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Status för användarhistorien" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Poäng" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Status för uppgifter" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Status för ärenden" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Ärendetyper" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Prioritet" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Allvarsgrad" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Roller" diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po index 59c3af70..15ea255e 100644 --- a/taiga/locale/tr/LC_MESSAGES/django.po +++ b/taiga/locale/tr/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/tr/)\n" @@ -200,7 +200,7 @@ msgstr "" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "Engellenmiş nesne" @@ -361,7 +361,7 @@ msgid "Error in filter params types." msgstr "Parametre tipleri filtresinde hata." #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "'project' değeri numerik olmalı." @@ -513,62 +513,6 @@ msgstr "İhtiyaç duyulan döküm dosyası" msgid "Invalid dump format" msgstr "Geçersiz döküm biçemi" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "İçeri aktarılan proje verisinde hata" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "özel öznitelikler içeri aktarılırken hata" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "İçeri aktarılan rollerde hata" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "İçeri aktarılan üyeliklerde hata" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "İçeri aktarılan sprintlerde hata" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "İçeri aktarılan wiki sayfalarında hata" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "İçeri aktarılan wiki bağlantılarında hata" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "İçeri aktarılan taleplerde hata" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "İçeri aktarılan kullanıcı hikayelerinde hata" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "İçeri aktarılan görevlerde hata" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "İçeri aktarılan etiketlerde hata" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "zaman çizelgesi içeri aktarılırken hata" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" bu projede bulunamadı" @@ -588,14 +532,103 @@ msgstr "Geçersiz özel alanlar içeriyor." msgid "Name duplicated for the project" msgstr "Aynı isimde proje bulunmakta" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "İçeri aktarılan proje verisinde hata" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "İçeri aktarılan rollerde hata" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "İçeri aktarılan üyeliklerde hata" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "özel öznitelikler içeri aktarılırken hata" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "İçeri aktarılan sprintlerde hata" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "İçeri aktarılan kullanıcı hikayelerinde hata" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "İçeri aktarılan görevlerde hata" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "İçeri aktarılan taleplerde hata" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "İçeri aktarılan wiki sayfalarında hata" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "İçeri aktarılan wiki bağlantılarında hata" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "İçeri aktarılan etiketlerde hata" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "zaman çizelgesi içeri aktarılırken hata" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "Proje dökümü oluşturulurken hata" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "Proje dökümü yükleniyorken hata" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1273,6 +1306,15 @@ msgstr "Admin proje değerleri" msgid "Admin roles" msgstr "Yönetici rolleri" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "sahip" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "Eksik parametreq" @@ -1319,14 +1361,6 @@ msgstr "Kısmi güncellemeler desteklenmiyor" msgid "Project ID not matches between object and project" msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "sahip" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2544,39 +2578,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "Varsayılan ayarlar" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "Kullanıcı hikayelerinin durumları" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "Puanlar" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "Görevlerin durumları" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "Taleplerin durumları" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "Taleplerin tipleri" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "Öncelikler" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "Önem dereceleri" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "Roller" diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po index d357abfa..5537e07f 100644 --- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po +++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po @@ -11,8 +11,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-04-19 16:00+0200\n" -"PO-Revision-Date: 2016-04-19 14:00+0000\n" +"POT-Creation-Date: 2016-05-01 19:09+0200\n" +"PO-Revision-Date: 2016-05-01 17:09+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/zh-Hant/)\n" @@ -186,7 +186,7 @@ msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞" #: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 #: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:67 +#: taiga/webhooks/api.py:68 msgid "Blocked element" msgstr "" @@ -347,7 +347,7 @@ msgid "Error in filter params types." msgstr "過濾參數類型出錯" #: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:59 +#: taiga/projects/filters.py:63 msgid "'project' must be an integer value." msgstr "專案須為整數值" @@ -510,62 +510,6 @@ msgstr "需要的堆存檔案" msgid "Invalid dump format" msgstr "無效堆存格式" -#: taiga/export_import/dump_service.py:112 -msgid "error importing project data" -msgstr "滙入重要專案資料出錯" - -#: taiga/export_import/dump_service.py:125 -msgid "error importing lists of project attributes" -msgstr "滙入標籤出錯" - -#: taiga/export_import/dump_service.py:130 -msgid "error importing default project attributes values" -msgstr "滙入預設專案屬性數值出錯" - -#: taiga/export_import/dump_service.py:140 -msgid "error importing custom attributes" -msgstr "滙入客制性屬出錯" - -#: taiga/export_import/dump_service.py:145 -msgid "error importing roles" -msgstr "滙入角色出錯" - -#: taiga/export_import/dump_service.py:160 -msgid "error importing memberships" -msgstr "滙入成員資格出錯" - -#: taiga/export_import/dump_service.py:165 -msgid "error importing sprints" -msgstr "滙入衝刺任務出錯" - -#: taiga/export_import/dump_service.py:170 -msgid "error importing wiki pages" -msgstr "滙入維基頁出錯" - -#: taiga/export_import/dump_service.py:175 -msgid "error importing wiki links" -msgstr "滙入維基連結出錯" - -#: taiga/export_import/dump_service.py:180 -msgid "error importing issues" -msgstr "滙入問題出錯" - -#: taiga/export_import/dump_service.py:185 -msgid "error importing user stories" -msgstr "滙入使用者故事出錯" - -#: taiga/export_import/dump_service.py:190 -msgid "error importing tasks" -msgstr "滙入任務出錯" - -#: taiga/export_import/dump_service.py:195 -msgid "error importing tags" -msgstr "滙入標籤出錯" - -#: taiga/export_import/dump_service.py:199 -msgid "error importing timelines" -msgstr "滙入時間軸出錯" - #: taiga/export_import/serializers.py:178 msgid "{}=\"{}\" not found in this project" msgstr "{}=\"{}\" 無法在此專案中找到" @@ -585,14 +529,103 @@ msgstr "包括無效慣例欄位" msgid "Name duplicated for the project" msgstr "專案的名稱被複製了" -#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56 +#: taiga/export_import/services/store.py:621 +#: taiga/export_import/services/store.py:639 +msgid "error importing project data" +msgstr "滙入重要專案資料出錯" + +#: taiga/export_import/services/store.py:646 +msgid "error importing roles" +msgstr "滙入角色出錯" + +#: taiga/export_import/services/store.py:651 +msgid "error importing memberships" +msgstr "滙入成員資格出錯" + +#: taiga/export_import/services/store.py:661 +msgid "error importing lists of project attributes" +msgstr "滙入標籤出錯" + +#: taiga/export_import/services/store.py:665 +msgid "error importing default project attributes values" +msgstr "滙入預設專案屬性數值出錯" + +#: taiga/export_import/services/store.py:674 +msgid "error importing custom attributes" +msgstr "滙入客制性屬出錯" + +#: taiga/export_import/services/store.py:679 +msgid "error importing sprints" +msgstr "滙入衝刺任務出錯" + +#: taiga/export_import/services/store.py:683 +msgid "error importing user stories" +msgstr "滙入使用者故事出錯" + +#: taiga/export_import/services/store.py:687 +msgid "error importing tasks" +msgstr "滙入任務出錯" + +#: taiga/export_import/services/store.py:691 +msgid "error importing issues" +msgstr "滙入問題出錯" + +#: taiga/export_import/services/store.py:695 +msgid "error importing wiki pages" +msgstr "滙入維基頁出錯" + +#: taiga/export_import/services/store.py:699 +msgid "error importing wiki links" +msgstr "滙入維基連結出錯" + +#: taiga/export_import/services/store.py:703 +msgid "error importing tags" +msgstr "滙入標籤出錯" + +#: taiga/export_import/services/store.py:707 +msgid "error importing timelines" +msgstr "滙入時間軸出錯" + +#: taiga/export_import/services/store.py:731 +msgid "unexpected error importing project" +msgstr "" + +#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" msgstr "產生專案傾倒時出錯" -#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89 +#: taiga/export_import/tasks.py:81 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:110 msgid "Error loading project dump" msgstr "載入專案傾倒時出錯" +#: taiga/export_import/tasks.py:111 +msgid "Error loading your project dump file" +msgstr "" + +#: taiga/export_import/tasks.py:125 +msgid " -- no detail info --" +msgstr "" + #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format msgid "" @@ -1299,6 +1332,15 @@ msgstr "管理員專案數值" msgid "Admin roles" msgstr "管理員角色" +#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 +#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 +#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 +#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 +#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 +#: taiga/userstorage/models.py:26 +msgid "owner" +msgstr "所有者" + #: taiga/projects/api.py:165 taiga/users/api.py:220 msgid "Incomplete arguments" msgstr "不完整參數" @@ -1345,14 +1387,6 @@ msgstr "不支援部份更新" msgid "Project ID not matches between object and project" msgstr "專案ID不符合物件與專案" -#: taiga/projects/attachments/models.py:38 taiga/projects/issues/models.py:39 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:162 -#: taiga/projects/notifications/models.py:61 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:36 -#: taiga/users/admin.py:69 taiga/userstorage/models.py:26 -msgid "owner" -msgstr "所有者" - #: taiga/projects/attachments/models.py:40 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 @@ -2754,39 +2788,39 @@ msgstr "" msgid "At least one user must be an active admin for this project." msgstr "" -#: taiga/projects/serializers.py:392 +#: taiga/projects/serializers.py:396 msgid "Default options" msgstr "預設選項" -#: taiga/projects/serializers.py:393 +#: taiga/projects/serializers.py:397 msgid "User story's statuses" msgstr "使用者故事狀態" -#: taiga/projects/serializers.py:394 +#: taiga/projects/serializers.py:398 msgid "Points" msgstr "點數" -#: taiga/projects/serializers.py:395 +#: taiga/projects/serializers.py:399 msgid "Task's statuses" msgstr "任務狀態" -#: taiga/projects/serializers.py:396 +#: taiga/projects/serializers.py:400 msgid "Issue's statuses" msgstr "問題狀態" -#: taiga/projects/serializers.py:397 +#: taiga/projects/serializers.py:401 msgid "Issue's types" msgstr "問題類型" -#: taiga/projects/serializers.py:398 +#: taiga/projects/serializers.py:402 msgid "Priorities" msgstr "優先性" -#: taiga/projects/serializers.py:399 +#: taiga/projects/serializers.py:403 msgid "Severities" msgstr "嚴重性" -#: taiga/projects/serializers.py:400 +#: taiga/projects/serializers.py:404 msgid "Roles" msgstr "角色" From af3fedee426ad5a20fff47fadf6144d83563a9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 3 May 2016 11:51:01 +0200 Subject: [PATCH 004/261] Make is_private editable in the admin panel --- taiga/projects/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 44a2b412..4eb6d5d6 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -73,7 +73,7 @@ class ProjectAdmin(admin.ModelAdmin): "owner_url", "blocked_code", "is_featured"] list_display_links = ["id", "name", "slug"] list_filter = ("is_private", "blocked_code", "is_featured") - list_editable = ["is_featured", "blocked_code"] + list_editable = ["is_private", "is_featured", "blocked_code"] search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"] inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline] From a8d52cce01811f49e69adf05db813b9e21279fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 3 May 2016 13:05:32 +0200 Subject: [PATCH 005/261] Create actions to make projects public or private --- taiga/projects/admin.py | 43 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 4eb6d5d6..99757145 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -17,9 +17,11 @@ from django.contrib import admin from django.core.urlresolvers import reverse +from django.db import transaction from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ +from taiga.permissions import permissions from taiga.projects.milestones.admin import MilestoneInline from taiga.projects.notifications.admin import NotifyPolicyInline from taiga.projects.likes.admin import LikeInline @@ -73,7 +75,7 @@ class ProjectAdmin(admin.ModelAdmin): "owner_url", "blocked_code", "is_featured"] list_display_links = ["id", "name", "slug"] list_filter = ("is_private", "blocked_code", "is_featured") - list_editable = ["is_private", "is_featured", "blocked_code"] + list_editable = ["is_featured", "blocked_code"] search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"] inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline] @@ -121,8 +123,45 @@ class ProjectAdmin(admin.ModelAdmin): obj.delete_related_content() super().delete_model(request, obj) -# User Stories common admins + ## Actions + actions = [ + "make_public", + "make_private" + ] + @transaction.atomic + def make_public(self, request, queryset): + total_updates = 0 + for project in queryset.exclude(is_private=False): + project.is_private = False + + anon_permissions = list(map(lambda perm: perm[0], permissions.ANON_PERMISSIONS)) + project.anon_permissions = list(set((project.anon_permissions or []) + anon_permissions)) + project.public_permissions = list(set((project.public_permissions or []) + anon_permissions)) + + project.save() + total_updates += 1 + + self.message_user(request, _("{count} successfully made public.").format(count=total_updates)) + make_public.short_description = _("Make public") + + @transaction.atomic + def make_private(self, request, queryset): + total_updates = 0 + + for project in queryset.exclude(is_private=True): + project.is_private = True + project.anon_permissions = [] + project.public_permissions = [] + + project.save() + total_updates += 1 + + self.message_user(request, _("{count} successfully made private.").format(count=total_updates)) + make_private.short_description = _("Make private") + + +# User Stories common admins class PointsAdmin(admin.ModelAdmin): list_display = ["project", "order", "name", "value"] list_display_links = ["name"] From 8791d5d10343a332182260595bb7eb4e723a22bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 3 May 2016 20:57:44 +0200 Subject: [PATCH 006/261] Fix error importing user stories with related issues --- taiga/export_import/services/store.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index e286c97c..fe34ff2a 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -673,11 +673,14 @@ def _populate_project_object(project, data): serializers.IssueCustomAttributeExportSerializer) check_if_there_is_some_error(_("error importing custom attributes"), project) - # Create milestones store_milestones(project, data) check_if_there_is_some_error(_("error importing sprints"), project) + # Create issues + store_issues(project, data) + check_if_there_is_some_error(_("error importing issues"), project) + # Create user stories store_user_stories(project, data) check_if_there_is_some_error(_("error importing user stories"), project) @@ -686,10 +689,6 @@ def _populate_project_object(project, data): store_tasks(project, data) check_if_there_is_some_error(_("error importing tasks"), project) - # Create issues - store_issues(project, data) - check_if_there_is_some_error(_("error importing issues"), project) - # Create wiki pages store_wiki_pages(project, data) check_if_there_is_some_error(_("error importing wiki pages"), project) From 2456c0454de97561fff5b410ba29753cf1911d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 3 May 2016 23:03:24 +0200 Subject: [PATCH 007/261] Improve the project admin --- taiga/projects/admin.py | 57 +++++++++++++++++++++++++-- taiga/projects/notifications/admin.py | 1 + 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 99757145..3349f360 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -22,13 +22,13 @@ from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from taiga.permissions import permissions -from taiga.projects.milestones.admin import MilestoneInline from taiga.projects.notifications.admin import NotifyPolicyInline from taiga.projects.likes.admin import LikeInline from taiga.users.admin import RoleInline from . import models + class MembershipAdmin(admin.ModelAdmin): list_display = ['project', 'role', 'user'] list_display_links = list_display @@ -49,6 +49,7 @@ class MembershipAdmin(admin.ModelAdmin): return super().formfield_for_foreignkey(db_field, request, **kwargs) + class MembershipInline(admin.TabularInline): model = models.Membership extra = 0 @@ -77,10 +78,58 @@ class ProjectAdmin(admin.ModelAdmin): list_filter = ("is_private", "blocked_code", "is_featured") list_editable = ["is_featured", "blocked_code"] search_fields = ["id", "name", "slug", "owner__username", "owner__email", "owner__full_name"] - inlines = [RoleInline, MembershipInline, MilestoneInline, NotifyPolicyInline, LikeInline] + inlines = [RoleInline, + MembershipInline, + NotifyPolicyInline, + LikeInline] - # NOTE: TextArrayField with a choices is broken in the admin panel. - exclude = ("anon_permissions", "public_permissions") + fieldsets = ( + (None, { + "fields": ("name", + "slug", + "is_featured", + "description", + "tags", + "logo", + ("created_date", "modified_date")) + }), + (_("Privacity"), { + "fields": (("owner", "blocked_code"), + "is_private", + ("anon_permissions", "public_permissions"), + "transfer_token") + }), + (_("Extra info"), { + "classes": ("collapse",), + "fields": ("creation_template", + ("is_looking_for_people", "looking_for_people_note"), + "tags_colors"), + }), + (_("Modules"), { + "classes": ("collapse",), + "fields": (("is_backlog_activated", "total_milestones", "total_story_points"), + "is_kanban_activated", + "is_issues_activated", + "is_wiki_activated", + ("videoconferences", "videoconferences_extra_data")), + }), + (_("Default values"), { + "classes": ("collapse",), + "fields": (("default_points", "default_us_status"), + "default_task_status", + ("default_issue_status", "default_priority", "default_severity", "default_issue_type")), + }), + (_("Activity"), { + "classes": ("collapse",), + "fields": (("total_activity", "total_activity_last_week", + "total_activity_last_month", "total_activity_last_year"),), + }), + (_("Fans"), { + "classes": ("collapse",), + "fields": (("total_fans", "total_fans_last_week", + "total_fans_last_month", "total_fans_last_year"),), + }), + ) def owner_url(self, obj): if obj.owner: diff --git a/taiga/projects/notifications/admin.py b/taiga/projects/notifications/admin.py index b35dd206..07a70396 100644 --- a/taiga/projects/notifications/admin.py +++ b/taiga/projects/notifications/admin.py @@ -27,6 +27,7 @@ class WatchedInline(GenericTabularInline): extra = 0 raw_id_fields = ["project", "user"] + class NotifyPolicyInline(TabularInline): model = models.NotifyPolicy extra = 0 From 4516dbca492c1ba436c0033f3ecb5ed68b1fb7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 5 May 2016 13:27:40 +0200 Subject: [PATCH 008/261] Fix issue #4151: @mentions with dashes doesn't work --- taiga/mdrender/extensions/mentions.py | 4 +-- tests/unit/test_mdrender.py | 41 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/taiga/mdrender/extensions/mentions.py b/taiga/mdrender/extensions/mentions.py index 683250d2..d4620449 100644 --- a/taiga/mdrender/extensions/mentions.py +++ b/taiga/mdrender/extensions/mentions.py @@ -31,10 +31,10 @@ from markdown.util import etree, AtomicString class MentionsExtension(Extension): def extendMarkdown(self, md, md_globals): - MENTION_RE = r'(@)([a-zA-Z0-9.-\._]+)' + MENTION_RE = r"(@)([\w.-]+)" mentionsPattern = MentionsPattern(MENTION_RE) mentionsPattern.md = md - md.inlinePatterns.add('mentions', mentionsPattern, '_end') + md.inlinePatterns.add("mentions", mentionsPattern, "_end") class MentionsPattern(Pattern): diff --git a/tests/unit/test_mdrender.py b/tests/unit/test_mdrender.py index 0c3fb202..73ce4233 100644 --- a/tests/unit/test_mdrender.py +++ b/tests/unit/test_mdrender.py @@ -27,6 +27,8 @@ dummy_project = MagicMock() dummy_project.id = 1 dummy_project.slug = "test" +dummy_uuser = MagicMock() +dummy_uuser.get_full_name.return_value = "Dummy User" def test_proccessor_valid_emoji(): result = emojify.EmojifyPreprocessor().run(["**:smile:**"]) @@ -38,6 +40,45 @@ def test_proccessor_invalid_emoji(): assert result == ["**:notvalidemoji:**"] +def test_mentions_valid_username(): + with patch("taiga.mdrender.extensions.mentions.get_user_model") as get_user_model_mock: + dummy_uuser = MagicMock() + dummy_uuser.get_full_name.return_value = "Hermione Granger" + get_user_model_mock.return_value.objects.get = MagicMock(return_value=dummy_uuser) + + result = render(dummy_project, "text @hermione text") + + get_user_model_mock.return_value.objects.get.assert_called_with(username="hermione") + assert result == ('

text @hermione text

') + + +def test_mentions_valid_username_with_points(): + with patch("taiga.mdrender.extensions.mentions.get_user_model") as get_user_model_mock: + dummy_uuser = MagicMock() + dummy_uuser.get_full_name.return_value = "Luna Lovegood" + get_user_model_mock.return_value.objects.get = MagicMock(return_value=dummy_uuser) + + result = render(dummy_project, "text @luna.lovegood text") + + get_user_model_mock.return_value.objects.get.assert_called_with(username="luna.lovegood") + assert result == ('

text @luna.lovegood text

') + + +def test_mentions_valid_username_with_dash(): + with patch("taiga.mdrender.extensions.mentions.get_user_model") as get_user_model_mock: + dummy_uuser = MagicMock() + dummy_uuser.get_full_name.return_value = "Ginny Weasley" + get_user_model_mock.return_value.objects.get = MagicMock(return_value=dummy_uuser) + + result = render(dummy_project, "text @super-ginny text") + + get_user_model_mock.return_value.objects.get.assert_called_with(username="super-ginny") + assert result == ('

text @super-ginny text

') + + def test_proccessor_valid_us_reference(): with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock: instance = mock.return_value From bdb5a433f899fb5c5ee30de40c2de5d064b3363c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 5 May 2016 13:32:06 +0200 Subject: [PATCH 009/261] Remove some unused vars --- tests/unit/test_mdrender.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/test_mdrender.py b/tests/unit/test_mdrender.py index 73ce4233..0ce3e5e9 100644 --- a/tests/unit/test_mdrender.py +++ b/tests/unit/test_mdrender.py @@ -27,8 +27,6 @@ dummy_project = MagicMock() dummy_project.id = 1 dummy_project.slug = "test" -dummy_uuser = MagicMock() -dummy_uuser.get_full_name.return_value = "Dummy User" def test_proccessor_valid_emoji(): result = emojify.EmojifyPreprocessor().run(["**:smile:**"]) From ec435f45bcb90389e569b059bcc7852c9c9eb5eb Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 5 May 2016 06:34:27 +0200 Subject: [PATCH 010/261] Import export projects with None as name for ProjectRelatedField --- taiga/export_import/serializers.py | 1 + tests/integration/test_importer_api.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 55b2031d..a8856f2f 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -160,6 +160,7 @@ class CommentField(serializers.WritableField): class ProjectRelatedField(serializers.RelatedField): read_only = False + null_values = (None, "") def __init__(self, slug_field, *args, **kwargs): self.slug_field = slug_field diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index b9e2d1c7..aff155ff 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -1107,6 +1107,26 @@ def test_services_store_project_from_dict_with_no_members_public_project_slots_a assert "reaches your current limit of memberships for public" in str(excinfo.value) +def test_services_store_project_from_dict_with_issue_priorities_names_as_None(client): + user = f.UserFactory.create() + data = { + "name": "Imported project", + "description": "Imported project", + "issue_types": [{"name": "Bug"}], + "issue_statuses": [{"name": "New"}], + "priorities": [{"name": "None", "order": 5, "color": "#CC0000"}], + "severities": [{"name": "Normal", "order": 5, "color": "#CC0000"}], + "issues": [{ + "status": "New", + "priority": "None", + "severity": "Normal", + "type": "Bug", + "subject": "Test"}]} + + project = services.store_project_from_dict(data, owner=user) + assert project.issues.first().priority.name == "None" + + ################################################################## ## tes api/v1/importer/load-dummp ################################################################## @@ -1701,5 +1721,3 @@ def test_dump_import_duplicated_project(client): assert response.status_code == 201 assert response.data["name"] == "Test import" assert response.data["slug"] == "{}-test-import".format(user.username) - - From eac0ee89cdb59b8880e7bc08ed14cb52821604d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 6 May 2016 10:38:04 +0200 Subject: [PATCH 011/261] Improve the logs when an importer process fail --- taiga/export_import/services/store.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index fe34ff2a..dc521b26 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -618,7 +618,8 @@ def _create_project_object(data): project_serialized = store_project(data) if not project_serialized: - raise err.TaigaImportError(_("error importing project data"), None) + errors = get_errors(clear=True) + raise err.TaigaImportError(_("error importing project data"), None, errors=errors) return project_serialized.object if project_serialized else None @@ -637,7 +638,7 @@ def _create_membership_for_project_owner(project): def _populate_project_object(project, data): def check_if_there_is_some_error(message=_("error importing project data"), project=None): - errors = get_errors(clear=False) + errors = get_errors(clear=True) if errors: raise err.TaigaImportError(message, project, errors=errors) @@ -710,8 +711,6 @@ def _populate_project_object(project, data): def store_project_from_dict(data, owner=None): - reset_errors() - # Validate if owner: _validate_if_owner_have_enought_space_to_this_project(owner, data) From 9e897ca49999d11422463e3cf1cd0c3386cd1f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 6 May 2016 16:42:03 +0200 Subject: [PATCH 012/261] Improve the import command --- taiga/export_import/management/commands/load_dump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index 08f7811b..fe6c4f9a 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -69,4 +69,4 @@ class Command(BaseCommand): print("ERROR:", end=" ") print(e.message) - print(services.store.get_errors()) + print(json.dumps(e.errors, indent=4)) From 576700afb6b7f092b07b71b4952b48d897b85013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 9 May 2016 11:47:36 +0200 Subject: [PATCH 013/261] Fix error, import a bad model class --- taiga/projects/tasks/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index e1f76f67..2736e317 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -89,7 +89,7 @@ def snapshot_tasks_in_bulk(bulk_data, user): try: task = models.Task.objects.get(pk=task_data['task_id']) take_snapshot(task, user=user) - except models.UserStory.DoesNotExist: + except models.Task.DoesNotExist: pass From 85acb93e22c369ada2132e4da983916b70e77337 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 11 May 2016 15:02:02 +0200 Subject: [PATCH 014/261] Improving performance for history api --- taiga/projects/history/api.py | 2 +- taiga/projects/history/models.py | 25 ++++++++++++++++++------- taiga/projects/history/services.py | 13 ++++++++++++- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 9d958d0e..d10194ce 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -102,8 +102,8 @@ class HistoryViewSet(ReadOnlyListViewSet): def retrieve(self, request, pk): obj = self.get_object() self.check_permissions(request, "retrieve", obj) - qs = services.get_history_queryset_by_model_instance(obj) + qs = services.prefetch_owners_in_history_queryset(qs) return self.response_for_queryset(qs) diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index d440900c..e947c6fe 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -78,6 +78,8 @@ class HistoryEntry(models.Model): is_snapshot = models.BooleanField(default=False) _importing = None + _owner = None + _prefetched_owner = False @cached_property def is_change(self): @@ -91,14 +93,23 @@ class HistoryEntry(models.Model): def is_delete(self): return self.type == HistoryType.delete - @cached_property + @property def owner(self): - pk = self.user["pk"] - model = get_user_model() - try: - return model.objects.get(pk=pk) - except model.DoesNotExist: - return None + if not self._prefetched_owner: + pk = self.user["pk"] + model = get_user_model() + try: + owner = model.objects.get(pk=pk) + except model.DoesNotExist: + owner = None + + self.prefetch_owner(owner) + + return self._owner + + def prefetch_owner(self, owner): + self._owner = owner + self._prefetched_owner = True @cached_property def values_diff(self): diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 399b486f..22839ec8 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -33,6 +33,7 @@ from functools import wraps from functools import lru_cache from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.core.paginator import Paginator, InvalidPage from django.apps import apps @@ -331,7 +332,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): "is_hidden": is_hidden, "is_snapshot": need_real_snapshot, } - + return entry_model.objects.create(**kwargs) @@ -352,6 +353,16 @@ def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change return qs.order_by("created_at") +def prefetch_owners_in_history_queryset(qs): + user_ids = [u["pk"] for u in qs.values_list("user", flat=True)] + users = get_user_model().objects.filter(id__in=user_ids) + users_by_id = {u.id: u for u in users} + for history_entry in qs: + history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None)) + + return qs + + # Freeze implementatitions from .freeze_impl import project_freezer from .freeze_impl import milestone_freezer From 9474a1174f34d7d90e3046588cd26bd061e92dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 10 May 2016 16:06:03 +0200 Subject: [PATCH 015/261] Made minor fixes over load_dump and dump_project commands --- .../management/commands/dump_project.py | 6 +-- .../management/commands/load_dump.py | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py index d1248ad4..0b8938bc 100644 --- a/taiga/export_import/management/commands/dump_project.py +++ b/taiga/export_import/management/commands/dump_project.py @@ -24,7 +24,7 @@ import os class Command(BaseCommand): - help = "Export projects to json" + help = "Export projects to a json file" def add_arguments(self, parser): parser.add_argument("project_slugs", @@ -56,7 +56,7 @@ class Command(BaseCommand): raise CommandError("Project '{}' does not exist".format(project_slug)) dst_file = os.path.join(dst_dir, "{}.json".format(project_slug)) - with open(src_file, "w") as f: + with open(dst_file, "w") as f: render_project(project, f) - print("-> Generate dump of project '{}' in '{}'".format(project.name, src_file)) + print("-> Generate dump of project '{}' in '{}'".format(project.name, dst_file)) diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index fe6c4f9a..61209862 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -18,34 +18,39 @@ from django.core.management.base import BaseCommand from django.db import transaction from django.db.models import signals -from optparse import make_option from taiga.base.utils import json from taiga.export_import import services from taiga.export_import import exceptions as err -from taiga.export_import.renderers import ExportRenderer from taiga.projects.models import Project from taiga.users.models import User class Command(BaseCommand): - args = ' ' - help = 'Export a project to json' - renderer_context = {"indent": 4} - renderer = ExportRenderer() - option_list = BaseCommand.option_list + ( - make_option('--overwrite', - action='store_true', - dest='overwrite', - default=False, - help='Delete project if exists'), - ) + help = 'Import a project from a json file' + + def add_arguments(self, parser): + parser.add_argument("dump_file", + help="The path to a dump file (.json).") + + parser.add_argument("owner_email", + help="The email of the new project owner.") + + parser.add_argument("-o", '--overwrite', + action='store_true', + dest='overwrite', + default=False, + help='Overwrite the project if exists') def handle(self, *args, **options): - data = json.loads(open(args[0], 'r').read()) + dump_file_path = options["dump_file"] + owner_email = options["owner_email"] + overwrite = options["overwrite"] + + data = json.loads(open(dump_file_path, 'r').read()) try: with transaction.atomic(): - if options["overwrite"]: + if overwrite: receivers_back = signals.post_delete.receivers signals.post_delete.receivers = [] try: @@ -60,7 +65,7 @@ class Command(BaseCommand): pass signals.post_delete.receivers = receivers_back - user = User.objects.get(email=args[1]) + user = User.objects.get(email=owner_email) services.store_project_from_dict(data, user) except err.TaigaImportError as e: if e.project: From 4d1fb120b13287b791f625fa615c20c7f309ee27 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 11 May 2016 12:41:16 +0200 Subject: [PATCH 016/261] Improving performance when updating objects --- taiga/permissions/service.py | 27 ++++++++++++++----- taiga/projects/models.py | 30 +++++++++++++++++++-- taiga/projects/notifications/services.py | 23 +++++------------ taiga/projects/references/models.py | 3 --- taiga/projects/services/tags_colors.py | 2 +- taiga/timeline/service.py | 29 +++++++++++++++++++-- taiga/timeline/signals.py | 33 +++--------------------- taiga/webhooks/apps.py | 2 -- tests/integration/test_notifications.py | 21 +++++++++------ tests/unit/test_timeline.py | 8 +++--- 10 files changed, 105 insertions(+), 73 deletions(-) diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py index a56b3afc..32f79fdc 100644 --- a/taiga/permissions/service.py +++ b/taiga/permissions/service.py @@ -20,11 +20,18 @@ from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIO from django.apps import apps -def _get_user_project_membership(user, project): +def _get_user_project_membership(user, project, cache="user"): + """ + cache param determines how memberships are calculated trying to reuse the existing data + in cache + """ if user.is_anonymous(): return None - return user.cached_membership_for_project(project) + if cache == "user": + return user.cached_membership_for_project(project) + + return project.cached_memberships_for_user(user) def _get_object_project(obj): @@ -63,13 +70,17 @@ def is_project_admin(user, obj): return False -def user_has_perm(user, perm, obj=None): +def user_has_perm(user, perm, obj=None, cache="user"): + """ + cache param determines how memberships are calculated trying to reuse the existing data + in cache + """ project = _get_object_project(obj) if not project: return False - return perm in get_user_project_permissions(user, project) + return perm in get_user_project_permissions(user, project, cache=cache) def role_has_perm(role, perm): @@ -82,8 +93,12 @@ def _get_membership_permissions(membership): return [] -def get_user_project_permissions(user, project): - membership = _get_user_project_membership(user, project) +def get_user_project_permissions(user, project, cache="user"): + """ + cache param determines how memberships are calculated trying to reuse the existing data + in cache + """ + membership = _get_user_project_membership(user, project, cache=cache) if user.is_superuser: admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 730a62e3..cd38c35b 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -44,7 +44,6 @@ from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.services import ( - get_notify_policy, set_notify_policy_level, set_notify_policy_level_to_ignore, create_notify_policy_if_not_exists) @@ -345,6 +344,33 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): def cached_user_stories(self): return list(self.user_stories.all()) + @cached_property + def cached_notify_policies(self): + return {np.user.id: np for np in self.notify_policies.select_related("user", "project")} + + def cached_notify_policy_for_user(self, user): + """ + Get notification level for specified project and user. + """ + policy = self.cached_notify_policies.get(user.id, None) + if policy is None: + model_cls = apps.get_model("notifications", "NotifyPolicy") + policy = model_cls.objects.create( + project=self, + user=user, + notify_level= NotifyLevel.involved) + + del self.cached_notify_policies + + return policy + + @cached_property + def cached_memberships(self): + return {m.user.id: m for m in self.memberships.exclude(user__isnull=True).select_related("user", "project", "role")} + + def cached_memberships_for_user(self, user): + return self.cached_memberships.get(user.id, None) + def get_roles(self): return self.roles.all() @@ -426,7 +452,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): set_notify_policy_level(notify_policy, notify_level) def remove_watcher(self, user): - notify_policy = get_notify_policy(self, user) + notify_policy = self.cached_notify_policy_for_user(user) set_notify_policy_level_to_ignore(notify_policy) def delete_related_content(self): diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 0a3cb8e7..22d518cb 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -78,16 +78,6 @@ def create_notify_policy_if_not_exists(project, user, level=NotifyLevel.involved raise exc.IntegrityError(_("Notify exists for specified user and project")) from e -def get_notify_policy(project, user): - """ - Get notification level for specified project and user. - """ - model_cls = apps.get_model("notifications", "NotifyPolicy") - instance, _ = model_cls.objects.get_or_create(project=project, user=user, - defaults={"notify_level": NotifyLevel.involved}) - return instance - - def analize_object_for_watchers(obj:object, comment:str, user:object): """ Generic implementation for analize model objects and @@ -124,13 +114,13 @@ def _filter_by_permissions(obj, user): WikiPage = apps.get_model("wiki", "WikiPage") if isinstance(obj, UserStory): - return user_has_perm(user, "view_us", obj) + return user_has_perm(user, "view_us", obj, cache="project") elif isinstance(obj, Issue): - return user_has_perm(user, "view_issues", obj) + return user_has_perm(user, "view_issues", obj, cache="project") elif isinstance(obj, Task): - return user_has_perm(user, "view_tasks", obj) + return user_has_perm(user, "view_tasks", obj, cache="project") elif isinstance(obj, WikiPage): - return user_has_perm(user, "view_wiki_pages", obj) + return user_has_perm(user, "view_wiki_pages", obj, cache="project") return False @@ -149,7 +139,7 @@ def get_users_to_notify(obj, *, discard_users=None) -> list: project = obj.get_project() def _check_level(project:object, user:object, levels:tuple) -> bool: - policy = get_notify_policy(project, user) + policy = project.cached_notify_policy_for_user(user) return policy.notify_level in levels _can_notify_hard = partial(_check_level, project, @@ -226,8 +216,7 @@ def send_notifications(obj, *, history): # Get a complete list of notifiable users for current # object and send the change notification to them. notify_users = get_users_to_notify(obj, discard_users=[notification.owner]) - for notify_user in notify_users: - notification.notify_users.add(notify_user) + notification.notify_users.add(*notify_users) # If we are the min interval is 0 it just work in a synchronous and spamming way if settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL == 0: diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py index bf3b072c..3bc63b1c 100644 --- a/taiga/projects/references/models.py +++ b/taiga/projects/references/models.py @@ -110,6 +110,3 @@ models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue") models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask") models.signals.post_delete.connect(delete_sequence, sender=Project, dispatch_uid="refprojdel") - - - diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py index 9cebd044..90d44efa 100644 --- a/taiga/projects/services/tags_colors.py +++ b/taiga/projects/services/tags_colors.py @@ -54,7 +54,7 @@ def update_project_tags_colors_handler(instance): new_color = _get_new_color(tag, settings.TAGS_PREDEFINED_COLORS, exclude=used_colors) instance.project.tags_colors.append([tag, new_color]) - + remove_unused_tags(instance.project) if not isinstance(instance, Project): diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 65e27a89..12f7f29e 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -78,8 +78,7 @@ def _add_to_objects_timeline(objects, instance:object, event_type:str, created_d _add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data) -@app.task -def push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): if isinstance(objects, Model): _add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data) elif isinstance(objects, QuerySet) or isinstance(objects, list): @@ -88,6 +87,32 @@ def push_to_timeline(objects, instance:object, event_type:str, created_datetime: raise Exception("Invalid objects parameter") +@app.task +def push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}): + if project is not None: + # Actions related with a project + + ## Project timeline + _push_to_timeline(project, obj, event_type, created_datetime, + namespace=build_project_namespace(project), + extra_data=extra_data) + + project.refresh_totals() + + if hasattr(obj, "get_related_people"): + related_people = obj.get_related_people() + + _push_to_timeline(related_people, obj, event_type, created_datetime, + namespace=build_user_namespace(user), + extra_data=extra_data) + else: + # Actions not related with a project + ## - Me + _push_to_timeline(user, obj, event_type, created_datetime, + namespace=build_user_namespace(user), + extra_data=extra_data) + + def get_timeline(obj, namespace=None): assert isinstance(obj, Model), "obj must be a instance of Model" from .models import Timeline diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 887688fc..eda08031 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -22,42 +22,17 @@ from django.utils.translation import ugettext as _ from taiga.projects.history import services as history_services from taiga.projects.history.choices import HistoryType -from taiga.timeline.service import (push_to_timeline, +from taiga.timeline.service import (push_to_timelines, build_user_namespace, build_project_namespace, extract_user_info) -def _push_to_timeline(*args, **kwargs): +def _push_to_timelines(*args, **kwargs): if settings.CELERY_ENABLED: - push_to_timeline.delay(*args, **kwargs) + push_to_timelines.delay(*args, **kwargs) else: - push_to_timeline(*args, **kwargs) - - -def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}): - if project is not None: - # Actions related with a project - - ## Project timeline - _push_to_timeline(project, obj, event_type, created_datetime, - namespace=build_project_namespace(project), - extra_data=extra_data) - - project.refresh_totals() - - if hasattr(obj, "get_related_people"): - related_people = obj.get_related_people() - - _push_to_timeline(related_people, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) - else: - # Actions not related with a project - ## - Me - _push_to_timeline(user, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + push_to_timelines(*args, **kwargs) def _clean_description_fields(values_diff): diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py index a10cd1d2..9fff8e9b 100644 --- a/taiga/webhooks/apps.py +++ b/taiga/webhooks/apps.py @@ -27,8 +27,6 @@ def connect_webhooks_signals(): dispatch_uid="webhooks") - - def disconnect_webhooks_signals(): signals.post_save.disconnect(sender=apps.get_model("history", "HistoryEntry"), dispatch_uid="webhooks") diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 85ea89fb..bb2cdfd8 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -60,7 +60,7 @@ def test_create_retrieve_notify_policy(): current_number = policy_model_cls.objects.all().count() assert current_number == 0 - policy = services.get_notify_policy(project, project.owner) + policy = project.cached_notify_policy_for_user(project.owner) current_number = policy_model_cls.objects.all().count() assert current_number == 1 @@ -182,6 +182,7 @@ def test_users_to_notify(): policy_member1.notify_level = NotifyLevel.all policy_member1.save() + del project.cached_notify_policies users = services.get_users_to_notify(issue) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} @@ -190,6 +191,8 @@ def test_users_to_notify(): issue.add_watcher(member3.user) policy_member3.notify_level = NotifyLevel.all policy_member3.save() + + del project.cached_notify_policies users = services.get_users_to_notify(issue) assert len(users) == 3 assert users == {member1.user, member3.user, issue.get_owner()} @@ -199,12 +202,14 @@ def test_users_to_notify(): policy_member3.save() issue.add_watcher(member3.user) + del project.cached_notify_policies users = services.get_users_to_notify(issue) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} # Test with watchers without permissions issue.add_watcher(member5.user) + del project.cached_notify_policies users = services.get_users_to_notify(issue) assert len(users) == 2 assert users == {member1.user, issue.get_owner()} @@ -231,7 +236,7 @@ def test_watching_users_to_notify_on_issue_modification_1(): issue = f.IssueFactory.create(project=project) watching_user = f.UserFactory() issue.add_watcher(watching_user) - watching_user_policy = services.get_notify_policy(project, watching_user) + watching_user_policy = project.cached_notify_policy_for_user(watching_user) issue.description = "test1" issue.save() watching_user_policy.notify_level = NotifyLevel.all @@ -250,7 +255,7 @@ def test_watching_users_to_notify_on_issue_modification_2(): issue = f.IssueFactory.create(project=project) watching_user = f.UserFactory() issue.add_watcher(watching_user) - watching_user_policy = services.get_notify_policy(project, watching_user) + watching_user_policy = project.cached_notify_policy_for_user(watching_user) issue.description = "test1" issue.save() watching_user_policy.notify_level = NotifyLevel.involved @@ -269,7 +274,7 @@ def test_watching_users_to_notify_on_issue_modification_3(): issue = f.IssueFactory.create(project=project) watching_user = f.UserFactory() issue.add_watcher(watching_user) - watching_user_policy = services.get_notify_policy(project, watching_user) + watching_user_policy = project.cached_notify_policy_for_user(watching_user) issue.description = "test1" issue.save() watching_user_policy.notify_level = NotifyLevel.none @@ -289,7 +294,7 @@ def test_watching_users_to_notify_on_issue_modification_4(): issue = f.IssueFactory.create(project=project) watching_user = f.UserFactory() project.add_watcher(watching_user) - watching_user_policy = services.get_notify_policy(project, watching_user) + watching_user_policy = project.cached_notify_policy_for_user(watching_user) issue.description = "test1" issue.save() watching_user_policy.notify_level = NotifyLevel.none @@ -309,7 +314,7 @@ def test_watching_users_to_notify_on_issue_modification_5(): issue = f.IssueFactory.create(project=project) watching_user = f.UserFactory() project.add_watcher(watching_user) - watching_user_policy = services.get_notify_policy(project, watching_user) + watching_user_policy = project.cached_notify_policy_for_user(watching_user) issue.description = "test1" issue.save() watching_user_policy.notify_level = NotifyLevel.all @@ -329,7 +334,7 @@ def test_watching_users_to_notify_on_issue_modification_6(): issue = f.IssueFactory.create(project=project) watching_user = f.UserFactory() project.add_watcher(watching_user) - watching_user_policy = services.get_notify_policy(project, watching_user) + watching_user_policy = project.cached_notify_policy_for_user(watching_user) issue.description = "test1" issue.save() watching_user_policy.notify_level = NotifyLevel.involved @@ -902,7 +907,7 @@ def test_watchers_assignation_for_us(client): def test_retrieve_notify_policies_by_anonymous_user(client): project = f.ProjectFactory.create() - policy = services.get_notify_policy(project, project.owner) + policy = project.cached_notify_policy_for_user(project.owner) url = reverse("notifications-detail", args=[policy.pk]) response = client.get(url, content_type="application/json") diff --git a/tests/unit/test_timeline.py b/tests/unit/test_timeline.py index e34906d6..18c679e5 100644 --- a/tests/unit/test_timeline.py +++ b/tests/unit/test_timeline.py @@ -26,12 +26,14 @@ from taiga.projects.models import Project import pytest +pytestmark = pytest.mark.django_db def test_push_to_timeline_many_objects(): with patch("taiga.timeline.service._add_to_object_timeline") as mock: users = [get_user_model(), get_user_model(), get_user_model()] + owner = get_user_model() project = Project() - service.push_to_timeline(users, project, "test", project.created_date) + service._push_to_timeline(users, project, "test", project.created_date) assert mock.call_count == 3 assert mock.mock_calls == [ call(users[0], project, "test", project.created_date, "default", {}), @@ -39,7 +41,7 @@ def test_push_to_timeline_many_objects(): call(users[2], project, "test", project.created_date, "default", {}), ] with pytest.raises(Exception): - service.push_to_timeline(None, project, "test") + service._push_to_timeline(None, project, "test") def test_add_to_objects_timeline(): @@ -54,7 +56,7 @@ def test_add_to_objects_timeline(): call(users[2], project, "test", project.created_date, "default", {}), ] with pytest.raises(Exception): - service.push_to_timeline(None, project, "test") + service._push_to_timeline(None, project, "test") def test_get_impl_key_from_model(): From a7dfec1fc9460ecf36cb792e2e3e33544156d8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 24 May 2016 14:17:52 +0200 Subject: [PATCH 017/261] Fix support pages url --- settings/sr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings/sr.py b/settings/sr.py index 4786e512..bc58e8e0 100644 --- a/settings/sr.py +++ b/settings/sr.py @@ -23,7 +23,7 @@ SR = { "github_url": "https://github.com/taigaio", }, "support": { - "url": "https://taiga.io/support", + "url": "https://tree.taiga.io/support", "email": "support@taiga.io", "mailing_list": "https://groups.google.com/forum/#!forum/taigaio", } From e295bd95d80632311645a4450241b27ef63e7b3c Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 24 May 2016 14:56:50 +0200 Subject: [PATCH 018/261] Ugly hack to temporary disable thumbnail generation for tiff files --- taiga/base/utils/thumbnails.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/taiga/base/utils/thumbnails.py b/taiga/base/utils/thumbnails.py index 2c2ffca0..1337ad84 100644 --- a/taiga/base/utils/thumbnails.py +++ b/taiga/base/utils/thumbnails.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import os + +from django.db.models.fields.files import FieldFile + from taiga.base.utils.urls import get_absolute_url from easy_thumbnails.files import get_thumbnailer @@ -22,6 +26,15 @@ from easy_thumbnails.exceptions import InvalidImageFormatError def get_thumbnail_url(file_obj, thumbnailer_size): + # Ugly hack to temporary ignore tiff files + relative_name = file_obj + if isinstance(file_obj, FieldFile): + relative_name = file_obj.name + + source_extension = os.path.splitext(relative_name)[1][1:] + if source_extension == "tiff": + return None + try: path_url = get_thumbnailer(file_obj)[thumbnailer_size].url thumb_url = get_absolute_url(path_url) From 827d1b61328a9a0270cce182769170f2424dca22 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 23 May 2016 13:22:45 +0200 Subject: [PATCH 019/261] Fixing timeline permissions for admin and superusers --- taiga/timeline/service.py | 14 +++++++++--- tests/integration/test_timeline.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 12f7f29e..11517542 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -128,6 +128,10 @@ def get_timeline(obj, namespace=None): def filter_timeline_for_user(timeline, user): + # Superusers can see everything + if user.is_superuser: + return timeline + # Filtering entities from public projects or entities without project tl_filter = Q(project__is_private=False) | Q(project=None) @@ -156,9 +160,13 @@ def filter_timeline_for_user(timeline, user): # Filtering private projects where user is member if not user.is_anonymous(): for membership in user.cached_memberships: - data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions])) - data_content_types.append(membership_content_type) - tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types) + # Admin roles can see everything in a project + if membership.is_admin: + tl_filter |= Q(project=membership.project) + else: + data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions])) + data_content_types.append(membership_content_type) + tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types) timeline = timeline.filter(tl_filter) return timeline diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 81912fdb..8c8e0182 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -130,6 +130,40 @@ def test_filter_timeline_private_project_member_permissions(): assert timeline.count() == 3 +def test_filter_timeline_private_project_member_admin(): + Timeline.objects.all().delete() + user1 = factories.UserFactory() + user2 = factories.UserFactory() + project = factories.ProjectFactory.create(is_private=True) + membership = factories.MembershipFactory.create(user=user2, project=project, is_admin=True) + task1= factories.TaskFactory() + task2= factories.TaskFactory.create(project=project) + + service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x))) + service._add_to_object_timeline(user1, task1, "test", task1.created_date) + service._add_to_object_timeline(user1, task2, "test", task2.created_date) + timeline = Timeline.objects.exclude(event_type="users.user.create") + timeline = service.filter_timeline_for_user(timeline, user2) + assert timeline.count() == 3 + + +def test_filter_timeline_private_project_member_superuser(): + Timeline.objects.all().delete() + user1 = factories.UserFactory() + user2 = factories.UserFactory(is_superuser=True) + project = factories.ProjectFactory.create(is_private=True) + + task1= factories.TaskFactory() + task2= factories.TaskFactory.create(project=project) + + service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x))) + service._add_to_object_timeline(user1, task1, "test", task1.created_date) + service._add_to_object_timeline(user1, task2, "test", task2.created_date) + timeline = Timeline.objects.exclude(event_type="users.user.create") + timeline = service.filter_timeline_for_user(timeline, user2) + assert timeline.count() == 2 + + def test_create_project_timeline(): project = factories.ProjectFactory.create(name="test project timeline") history_services.take_snapshot(project, user=project.owner) From 81b41529d2b6864ab69fe25c7fb46cab51d4ee94 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 9 May 2016 11:50:49 +0200 Subject: [PATCH 020/261] US #2929: Ability to edit commit and see their history --- CHANGELOG.md | 4 + taiga/projects/history/api.py | 84 +- .../migrations/0009_auto_20160512_1110.py | 26 + taiga/projects/history/models.py | 18 + taiga/projects/history/permissions.py | 27 +- taiga/projects/history/serializers.py | 2 + taiga/projects/history/services.py | 14 + taiga/timeline/signals.py | 5 +- .../test_history_resources.py | 897 +++++++++++++++++- tests/integration/test_history.py | 83 ++ 10 files changed, 1102 insertions(+), 58 deletions(-) create mode 100644 taiga/projects/history/migrations/0009_auto_20160512_1110.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b5f288a..eca3114a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog # +## 2.2.0 ??? (unreleased) +### Features +- [API] edit comment endpoint: comment owners and project admins can edit existing comments + ## 2.1.0 Ursus Americanus (2016-05-03) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index d10194ce..3f1ca240 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -23,6 +23,7 @@ from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ReadOnlyListViewSet from taiga.base.api.utils import get_object_or_404 +from taiga.mdrender.service import render as mdrender from . import permissions from . import serializers @@ -56,42 +57,93 @@ class HistoryViewSet(ReadOnlyListViewSet): return response.Ok(serializer.data) + @detail_route(methods=['get']) + def comment_versions(self, request, pk): + obj = self.get_object() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + + self.check_permissions(request, 'comment_versions', history_entry) + + if history_entry is None: + return response.NotFound() + + history_entry.attach_user_info_to_comment_versions() + return response.Ok(history_entry.comment_versions) + + @detail_route(methods=['post']) + def edit_comment(self, request, pk): + obj = self.get_object() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + obj = services.get_instance_from_key(history_entry.key) + comment = request.DATA.get("comment", None) + + self.check_permissions(request, 'edit_comment', history_entry) + + if history_entry is None: + return response.NotFound() + + if comment is None: + return response.BadRequest({"error": _("comment is required")}) + + if history_entry.delete_comment_date or history_entry.delete_comment_user: + return response.BadRequest({"error": _("deleted comments can't be edited")}) + + # comment_versions can be None if there are no historic versions of the comment + comment_versions = history_entry.comment_versions or [] + comment_versions.append({ + "date": history_entry.created_at, + "comment": history_entry.comment, + "comment_html": history_entry.comment_html, + "user": { + "id": request.user.pk, + } + }) + + history_entry.edit_comment_date = timezone.now() + history_entry.comment = comment + history_entry.comment_html = mdrender(obj.project, comment) + history_entry.comment_versions = comment_versions + history_entry.save() + return response.Ok() + @detail_route(methods=['post']) def delete_comment(self, request, pk): obj = self.get_object() - comment_id = request.QUERY_PARAMS.get('id', None) - comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() - self.check_permissions(request, 'delete_comment', comment) + self.check_permissions(request, 'delete_comment', history_entry) - if comment is None: + if history_entry is None: return response.NotFound() - if comment.delete_comment_date or comment.delete_comment_user: + if history_entry.delete_comment_date or history_entry.delete_comment_user: 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()} - comment.save() + history_entry.delete_comment_date = timezone.now() + history_entry.delete_comment_user = {"pk": request.user.pk, "name": request.user.get_full_name()} + history_entry.save() return response.Ok() @detail_route(methods=['post']) def undelete_comment(self, request, pk): obj = self.get_object() - comment_id = request.QUERY_PARAMS.get('id', None) - comment = services.get_history_queryset_by_model_instance(obj).filter(id=comment_id).first() + history_entry_id = request.QUERY_PARAMS.get('id', None) + history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() - self.check_permissions(request, 'undelete_comment', comment) + self.check_permissions(request, 'undelete_comment', history_entry) - if comment is None: + if history_entry is None: return response.NotFound() - if not comment.delete_comment_date and not comment.delete_comment_user: + if not history_entry.delete_comment_date and not history_entry.delete_comment_user: return response.BadRequest({"error": _("Comment not deleted")}) - comment.delete_comment_date = None - comment.delete_comment_user = None - comment.save() + history_entry.delete_comment_date = None + history_entry.delete_comment_user = None + history_entry.save() return response.Ok() # Just for restframework! Because it raises diff --git a/taiga/projects/history/migrations/0009_auto_20160512_1110.py b/taiga/projects/history/migrations/0009_auto_20160512_1110.py new file mode 100644 index 00000000..0cf39023 --- /dev/null +++ b/taiga/projects/history/migrations/0009_auto_20160512_1110.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-12 11:10 +from __future__ import unicode_literals + +from django.db import migrations, models +import django_pgjson.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0008_auto_20150508_1028'), + ] + + operations = [ + migrations.AddField( + model_name='historyentry', + name='comment_versions', + field=django_pgjson.fields.JsonField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='historyentry', + name='edit_comment_date', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index e947c6fe..39012f60 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -67,6 +67,10 @@ class HistoryEntry(models.Model): delete_comment_date = models.DateTimeField(null=True, blank=True, default=None) delete_comment_user = JsonField(null=True, blank=True, default=None) + # Historic version of comments + comment_versions = JsonField(null=True, blank=True, default=None) + edit_comment_date = models.DateTimeField(null=True, blank=True, default=None) + # Flag for mark some history entries as # hidden. Hidden history entries are important # for save but not important to preview. @@ -111,6 +115,20 @@ class HistoryEntry(models.Model): self._owner = owner self._prefetched_owner = True + def attach_user_info_to_comment_versions(self): + if not self.comment_versions: + return + + from taiga.users.serializers import UserSerializer + + user_ids = [v["user"]["id"] for v in self.comment_versions if "user" in v and "id" in v["user"]] + users_by_id = {u.id: u for u in get_user_model().objects.filter(id__in=user_ids)} + + for version in self.comment_versions: + user = users_by_id.get(version["user"]["id"], None) + if user: + version["user"] = UserSerializer(user).data + @cached_property def values_diff(self): result = {} diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index 015ac22c..fdce68cd 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -33,32 +33,41 @@ class IsCommentOwner(PermissionComponent): return obj.user and obj.user.get("pk", "not-pk") == request.user.pk -class IsCommentProjectOwner(PermissionComponent): +class IsCommentProjectAdmin(PermissionComponent): def check_permissions(self, request, view, obj=None): model = get_model_from_key(obj.key) pk = get_pk_from_key(obj.key) project = model.objects.get(pk=pk) return is_project_admin(request.user, project) + class UserStoryHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() class TaskHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() class IssueHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() class WikiHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') - delete_comment_perms = IsCommentProjectOwner() | IsCommentOwner() - undelete_comment_perms = IsCommentProjectOwner() | IsCommentDeleter() + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index 72b3c763..f231f29c 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -33,9 +33,11 @@ class HistoryEntrySerializer(serializers.ModelSerializer): values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) user = serializers.SerializerMethodField("get_user") delete_comment_user = JsonField() + comment_versions = JsonField() class Meta: model = models.HistoryEntry + exclude = ("comment_versions",) def get_user(self, entry): user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 22839ec8..61004471 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -91,6 +91,20 @@ def get_pk_from_key(key:str) -> object: return pk +def get_instance_from_key(key:str) -> object: + """ + Get instance from key + """ + model = get_model_from_key(key) + pk = get_pk_from_key(key) + try: + obj = model.objects.get(pk=pk) + return obj + except model.DoesNotExist: + # Catch simultaneous DELETE request + return None + + def register_values_implementation(typename:str, fn=None): """ Register values implementation for specified typename. diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index eda08031..c0f1dffa 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -44,7 +44,6 @@ def _clean_description_fields(values_diff): def on_new_history_entry(sender, instance, created, **kwargs): - if instance._importing: return @@ -81,6 +80,10 @@ def on_new_history_entry(sender, instance, created, **kwargs): if instance.delete_comment_date: extra_data["comment_deleted"] = True + # Detect edited comment + if instance.comment_versions is not None and len(instance.comment_versions)>0: + extra_data["comment_edited"] = True + created_datetime = instance.created_at _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data=extra_data) diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index b0991080..be17510c 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -1,6 +1,11 @@ from django.core.urlresolvers import reverse +from django.utils import timezone +from taiga.base.utils import json from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.projects.history.models import HistoryEntry +from taiga.projects.history.choices import HistoryType +from taiga.projects.history.services import make_key_from_model_object from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -21,11 +26,11 @@ def teardown_module(module): def data(): m = type("Models", (object,), {}) - m.registered_user = f.UserFactory.create() - m.project_member_with_perms = f.UserFactory.create() - m.project_member_without_perms = f.UserFactory.create() - m.project_owner = f.UserFactory.create() - m.other_user = f.UserFactory.create() + m.registered_user = f.UserFactory.create(full_name="registered_user") + m.project_member_with_perms = f.UserFactory.create(full_name="project_member_with_perms") + m.project_member_without_perms = f.UserFactory.create(full_name="project_member_without_perms") + m.project_owner = f.UserFactory.create(full_name="project_owner") + m.other_user = f.UserFactory.create(full_name="other_user") m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), @@ -76,39 +81,33 @@ def data(): return m +######################################################### +## User stories +######################################################### + + @pytest.fixture def data_us(data): m = type("Models", (object,), {}) m.public_user_story = f.UserStoryFactory(project=data.public_project, ref=1) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_user_story), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_user_story1 = f.UserStoryFactory(project=data.private_project1, ref=5) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_user_story1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9) - return m - - -@pytest.fixture -def data_task(data): - m = type("Models", (object,), {}) - m.public_task = f.TaskFactory(project=data.public_project, ref=2) - m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) - m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) - return m - - -@pytest.fixture -def data_issue(data): - m = type("Models", (object,), {}) - m.public_issue = f.IssueFactory(project=data.public_project, ref=3) - m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) - m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) - return m - - -@pytest.fixture -def data_wiki(data): - m = type("Models", (object,), {}) - m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) - m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) - m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_user_story2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) return m @@ -133,6 +132,222 @@ def test_user_story_history_retrieve(client, data, data_us): assert results == [401, 403, 403, 200, 200] +def test_user_story_action_edit_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-edit-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_user_story_action_delete_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-delete-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_us.public_history_entry.delete_comment_date = None + data_us.public_history_entry.delete_comment_user = None + data_us.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry1.delete_comment_date = None + data_us.private_history_entry1.delete_comment_user = None + data_us.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry2.delete_comment_date = None + data_us.private_history_entry2.delete_comment_user = None + data_us.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_user_story_action_undelete_comment(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-undelete-comment', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_us.public_history_entry.delete_comment_date = timezone.now() + data_us.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry1.delete_comment_date = timezone.now() + data_us.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_us.private_history_entry2.delete_comment_date = timezone.now() + data_us.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_us.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_user_story_action_comment_versions(client, data, data_us): + public_url = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.public_user_story.pk}), + data_us.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story1.pk}), + data_us.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('userstory-history-comment-versions', kwargs={"pk": data_us.private_user_story2.pk}), + data_us.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Tasks +######################################################### + + +@pytest.fixture +def data_task(data): + m = type("Models", (object,), {}) + m.public_task = f.TaskFactory(project=data.public_project, ref=2) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_task), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_task1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_task2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_task_history_retrieve(client, data, data_task): public_url = reverse('task-history-detail', kwargs={"pk": data_task.public_task.pk}) private_url1 = reverse('task-history-detail', kwargs={"pk": data_task.private_task1.pk}) @@ -154,6 +369,222 @@ def test_task_history_retrieve(client, data, data_task): assert results == [401, 403, 403, 200, 200] +def test_task_action_edit_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-edit-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_task_action_delete_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-delete-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_task.public_history_entry.delete_comment_date = None + data_task.public_history_entry.delete_comment_user = None + data_task.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry1.delete_comment_date = None + data_task.private_history_entry1.delete_comment_user = None + data_task.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry2.delete_comment_date = None + data_task.private_history_entry2.delete_comment_user = None + data_task.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_task_action_undelete_comment(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-undelete-comment', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_task.public_history_entry.delete_comment_date = timezone.now() + data_task.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry1.delete_comment_date = timezone.now() + data_task.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_task.private_history_entry2.delete_comment_date = timezone.now() + data_task.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_task.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_task_action_comment_versions(client, data, data_task): + public_url = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.public_task.pk}), + data_task.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task1.pk}), + data_task.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('task-history-comment-versions', kwargs={"pk": data_task.private_task2.pk}), + data_task.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Issues +######################################################### + + +@pytest.fixture +def data_issue(data): + m = type("Models", (object,), {}) + m.public_issue = f.IssueFactory(project=data.public_project, ref=3) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_issue), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_issue1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_issue2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_issue_history_retrieve(client, data, data_issue): public_url = reverse('issue-history-detail', kwargs={"pk": data_issue.public_issue.pk}) private_url1 = reverse('issue-history-detail', kwargs={"pk": data_issue.private_issue1.pk}) @@ -175,6 +606,222 @@ def test_issue_history_retrieve(client, data, data_issue): assert results == [401, 403, 403, 200, 200] +def test_issue_action_edit_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-edit-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_issue_action_delete_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-delete-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_issue.public_history_entry.delete_comment_date = None + data_issue.public_history_entry.delete_comment_user = None + data_issue.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry1.delete_comment_date = None + data_issue.private_history_entry1.delete_comment_user = None + data_issue.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry2.delete_comment_date = None + data_issue.private_history_entry2.delete_comment_user = None + data_issue.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_issue_action_undelete_comment(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-undelete-comment', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_issue.public_history_entry.delete_comment_date = timezone.now() + data_issue.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry1.delete_comment_date = timezone.now() + data_issue.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_issue.private_history_entry2.delete_comment_date = timezone.now() + data_issue.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_issue.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_issue_action_comment_versions(client, data, data_issue): + public_url = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.public_issue.pk}), + data_issue.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue1.pk}), + data_issue.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('issue-history-comment-versions', kwargs={"pk": data_issue.private_issue2.pk}), + data_issue.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## Wiki pages +######################################################### + + +@pytest.fixture +def data_wiki(data): + m = type("Models", (object,), {}) + m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing public", + key=make_key_from_model_object(m.public_wiki), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 1", + key=make_key_from_model_object(m.private_wiki1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing 2", + key=make_key_from_model_object(m.private_wiki2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + def test_wiki_history_retrieve(client, data, data_wiki): public_url = reverse('wiki-history-detail', kwargs={"pk": data_wiki.public_wiki.pk}) private_url1 = reverse('wiki-history-detail', kwargs={"pk": data_wiki.private_wiki1.pk}) @@ -194,3 +841,189 @@ def test_wiki_history_retrieve(client, data, data_wiki): assert results == [200, 200, 200, 200, 200] results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_wiki_action_edit_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-edit-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_action_delete_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-delete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_wiki.public_history_entry.delete_comment_date = None + data_wiki.public_history_entry.delete_comment_user = None + data_wiki.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry1.delete_comment_date = None + data_wiki.private_history_entry1.delete_comment_user = None + data_wiki.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry2.delete_comment_date = None + data_wiki.private_history_entry2.delete_comment_user = None + data_wiki.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_wiki_action_undelete_comment(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-undelete-comment', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_wiki.public_history_entry.delete_comment_date = timezone.now() + data_wiki.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry1.delete_comment_date = timezone.now() + data_wiki.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_wiki.private_history_entry2.delete_comment_date = timezone.now() + data_wiki.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_wiki.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_wiki_action_comment_versions(client, data, data_wiki): + public_url = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.public_wiki.pk}), + data_wiki.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki1.pk}), + data_wiki.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('wiki-history-comment-versions', kwargs={"pk": data_wiki.private_wiki2.pk}), + data_wiki.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index 01469c0a..5d936c0d 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -17,6 +17,8 @@ # along with this program. If not, see . import pytest +import datetime + from unittest.mock import patch from django.core.urlresolvers import reverse @@ -235,3 +237,84 @@ def test_delete_comment_by_project_owner(client): url = "%s?id=%s" % (url, history_entry.id) response = client.post(url, content_type="application/json") assert 200 == response.status_code, response.status_code + + +def test_edit_comment(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + comment="testing", + key=key, + diff={}, + user={"pk": project.owner.id}) + + history_entry_created_at = history_entry.created_at + assert history_entry.comment_versions == None + assert history_entry.edit_comment_date == None + + client.login(project.owner) + url = reverse("userstory-history-edit-comment", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + data = json.dumps({"comment": "testing update comment"}) + response = client.post(url, data, content_type="application/json") + assert 200 == response.status_code, response.status_code + + + history_entry = HistoryEntry.objects.get(id=history_entry.id) + assert len(history_entry.comment_versions) == 1 + assert history_entry.comment == "testing update comment" + assert history_entry.comment_versions[0]["comment"] == "testing" + assert history_entry.edit_comment_date != None + assert history_entry.comment_versions[0]["user"]["id"] == project.owner.id + + +def test_get_comment_versions(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create( + type=HistoryType.change, + comment="testing", + key=key, + diff={}, + user={"pk": project.owner.id}, + edit_comment_date=datetime.datetime.now(), + comment_versions = [{ + "comment_html": "

test

", + "date": "2016-05-09T09:34:27.221Z", + "comment": "test", + "user": { + "id": project.owner.id, + }}]) + + client.login(project.owner) + url = reverse("userstory-history-comment-versions", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + response = client.get(url, content_type="application/json") + assert 200 == response.status_code, response.status_code + assert response.data[0]["user"]["username"] == project.owner.username + + +def test_get_comment_versions_from_history_entry_without_comment(client): + project = f.create_project() + us = f.create_userstory(project=project) + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + key = make_key_from_model_object(us) + history_entry = f.HistoryEntryFactory.create( + type=HistoryType.change, + key=key, + diff={}, + user={"pk": project.owner.id}) + + client.login(project.owner) + url = reverse("userstory-history-comment-versions", args=(us.id,)) + url = "%s?id=%s" % (url, history_entry.id) + + response = client.get(url, content_type="application/json") + assert 200 == response.status_code, response.status_code + assert response.data == None From 591614e57adc877f93f9b87fced790e25188f3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 11 May 2016 20:36:33 +0200 Subject: [PATCH 021/261] Remove USER_PERMISSIONS --- taiga/permissions/permissions.py | 20 ----- taiga/permissions/service.py | 4 +- .../test_attachment_resources.py | 16 ++-- .../test_history_resources.py | 6 +- .../test_issues_custom_attributes_resource.py | 6 +- .../test_issues_resources.py | 38 ++++----- .../test_milestones_resources.py | 6 +- .../test_modules_resources.py | 6 +- .../test_resolver_resources.py | 6 +- .../test_search_resources.py | 6 +- .../test_tasks_custom_attributes_resource.py | 6 +- .../test_tasks_resources.py | 6 +- .../test_timelines_resources.py | 6 +- ..._userstories_custom_attributes_resource.py | 6 +- .../test_userstories_resources.py | 78 +++++++++---------- .../test_wiki_resources.py | 50 ++++++------ tests/integration/test_users.py | 4 +- tests/integration/test_watch_projects.py | 4 +- 18 files changed, 127 insertions(+), 147 deletions(-) diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py index edd24618..61c0b683 100644 --- a/taiga/permissions/permissions.py +++ b/taiga/permissions/permissions.py @@ -28,26 +28,6 @@ ANON_PERMISSIONS = [ ('view_wiki_links', _('View wiki links')), ] -USER_PERMISSIONS = [ - ('view_project', _('View project')), - ('view_milestones', _('View milestones')), - ('view_us', _('View user stories')), - ('view_issues', _('View issues')), - ('view_tasks', _('View tasks')), - ('view_wiki_pages', _('View wiki pages')), - ('view_wiki_links', _('View wiki links')), - ('request_membership', _('Request membership')), - ('add_us_to_project', _('Add user story to project')), - ('add_comments_to_us', _('Add comments to user stories')), - ('add_comments_to_task', _('Add comments to tasks')), - ('add_issue', _('Add issues')), - ('add_comments_to_issue', _('Add comments to issues')), - ('add_wiki_page', _('Add wiki page')), - ('modify_wiki_page', _('Modify wiki page')), - ('add_wiki_link', _('Add wiki link')), - ('modify_wiki_link', _('Modify wiki link')), -] - MEMBERS_PERMISSIONS = [ ('view_project', _('View project')), # Milestone permissions diff --git a/taiga/permissions/service.py b/taiga/permissions/service.py index 32f79fdc..02922574 100644 --- a/taiga/permissions/service.py +++ b/taiga/permissions/service.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS from django.apps import apps @@ -102,7 +102,7 @@ def get_user_project_permissions(user, project, cache="user"): if user.is_superuser: admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) - public_permissions = list(map(lambda perm: perm[0], USER_PERMISSIONS)) + public_permissions = [] anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) elif membership: if membership.is_admin: diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py index 0395d8b4..c48a7e3a 100644 --- a/tests/integration/resources_permissions/test_attachment_resources.py +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -4,7 +4,7 @@ from django.test.client import MULTIPART_CONTENT from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.projects.attachments.serializers import AttachmentSerializer @@ -38,11 +38,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -491,9 +491,9 @@ def test_wiki_attachment_patch(client, data, data_wiki): attachment_data = json.dumps(attachment_data) results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) @@ -583,9 +583,9 @@ def test_wiki_attachment_delete(client, data, data_wiki): ] results = helper_test_http_method(client, 'delete', public_url, None, [None, data.registered_user]) - assert results == [401, 204] + assert results == [401, 403] results = helper_test_http_method(client, 'delete', private_url1, None, [None, data.registered_user]) - assert results == [401, 204] + assert results == [401, 403] results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] results = helper_test_http_method(client, 'delete', blocked_url, None, users) @@ -721,7 +721,7 @@ def test_wiki_attachment_create(client, data, data_wiki): content_type=MULTIPART_CONTENT, after_each_request=_after_each_request_hook) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] attachment_data = {"description": "test", "object_id": data_wiki.blocked_wiki_attachment.object_id, diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index be17510c..9d39b9d7 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse from django.utils import timezone from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.history.models import HistoryEntry from taiga.projects.history.choices import HistoryType from taiga.projects.history.services import make_key_from_model_object @@ -34,11 +34,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 2c90cbfc..718172df 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -22,7 +22,7 @@ from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, - ANON_PERMISSIONS, USER_PERMISSIONS) + ANON_PERMISSIONS) from tests import factories as f from tests.utils import helper_test_http_method @@ -43,11 +43,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 2a6d3974..4cf3e2a1 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse from taiga.projects import choices as project_choices from taiga.projects.issues.serializers import IssueSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json from tests import factories as f @@ -39,12 +39,12 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) m.private_project2 = f.ProjectFactory(is_private=True, @@ -402,7 +402,7 @@ def test_issue_create(client, data): "type": data.public_project.issue_types.all()[0].pk, }) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({ "subject": "test", @@ -414,7 +414,7 @@ def test_issue_create(client, data): "type": data.private_project1.issue_types.all()[0].pk, }) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({ "subject": "test", @@ -456,21 +456,21 @@ def test_issue_patch(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"subject": "test", "version": data.public_issue.version}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.public_issue.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.private_issue1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.private_issue2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] + patch_data = json.dumps({"subject": "test", "version": data.blocked_issue.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] def test_issue_bulk_create(client, data): @@ -511,12 +511,12 @@ def test_issue_bulk_create(client, data): bulk_data = json.dumps({"bulk_issues": "test1\ntest2", "project_id": data.public_issue.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_issues": "test1\ntest2", "project_id": data.private_issue1.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_issues": "test1\ntest2", "project_id": data.private_issue2.project.pk}) diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index 5a6f26a4..c42c0580 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -6,7 +6,7 @@ from taiga.projects import choices as project_choices from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone from taiga.projects.notifications.services import add_watcher -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -35,11 +35,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py index 8260bd2f..27269458 100644 --- a/tests/integration/resources_permissions/test_modules_resources.py +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -2,7 +2,7 @@ import uuid from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json from tests import factories as f @@ -38,11 +38,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index f878ca14..6858d976 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -29,12 +29,12 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, slug="public") m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, slug="private1") m.private_project2 = f.ProjectFactory(is_private=True, diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index 8d3d9442..783818d3 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method_and_keys, disconnect_signals, reconnect_signals @@ -29,11 +29,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index 1fd33e46..44509354 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -22,7 +22,7 @@ from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, - ANON_PERMISSIONS, USER_PERMISSIONS) + ANON_PERMISSIONS) from tests import factories as f from tests.utils import helper_test_http_method @@ -43,11 +43,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 4771d12c..274f7a0c 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.tasks.serializers import TaskSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f @@ -39,12 +39,12 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) m.private_project2 = f.ProjectFactory(is_private=True, diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py index 0a874443..ffc4e41d 100644 --- a/tests/integration/resources_permissions/test_timelines_resources.py +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals @@ -29,11 +29,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index 9e6bd6ff..030060f3 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -22,7 +22,7 @@ from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, - ANON_PERMISSIONS, USER_PERMISSIONS) + ANON_PERMISSIONS) from tests import factories as f @@ -44,11 +44,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 80559a87..0daadad5 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.userstories.serializers import UserStorySerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f @@ -39,12 +39,12 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) m.private_project2 = f.ProjectFactory(is_private=True, @@ -177,29 +177,29 @@ def test_user_story_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - user_story_data = UserStorySerializer(data.public_user_story).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', public_url, user_story_data, users) - assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] - user_story_data = UserStorySerializer(data.private_user_story1).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) - assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] - user_story_data = UserStorySerializer(data.private_user_story2).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) - assert results == [401, 403, 403, 200, 200] + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] - user_story_data = UserStorySerializer(data.blocked_user_story).data - user_story_data["subject"] = "test" - user_story_data = json.dumps(user_story_data) - results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) - assert results == [401, 403, 403, 451, 451] + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["subject"] = "test" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] def test_user_story_update_with_project_change(client): user1 = f.UserFactory.create() @@ -361,11 +361,11 @@ def test_user_story_create(client, data): create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk}) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk}) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk}) results = helper_test_http_method(client, 'post', url, create_data, users) @@ -391,21 +391,21 @@ def test_user_story_patch(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.public_user_story.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.private_user_story1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"subject": "test", "version": data.private_user_story2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] + patch_data = json.dumps({"subject": "test", "version": data.blocked_user_story.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] def test_user_story_action_bulk_create(client, data): @@ -421,11 +421,11 @@ def test_user_story_action_bulk_create(client, data): bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.public_user_story.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.private_user_story1.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] bulk_data = json.dumps({"bulk_stories": "test1\ntest2", "project_id": data.private_user_story2.project.pk}) results = helper_test_http_method(client, 'post', url, bulk_data, users) diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index c69fb2bc..ed76e769 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -1,7 +1,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.projects.notifications.services import add_watcher from taiga.projects.occ import OCCResourceMixin @@ -37,11 +37,11 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], @@ -154,13 +154,13 @@ def test_wiki_page_update(client, data): wiki_page_data["content"] = "test" wiki_page_data = json.dumps(wiki_page_data) results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data wiki_page_data["content"] = "test" wiki_page_data = json.dumps(wiki_page_data) results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data wiki_page_data["content"] = "test" @@ -244,7 +244,7 @@ def test_wiki_page_create(client, data): "project": data.public_project.pk, }) results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({ "content": "test", @@ -252,7 +252,7 @@ def test_wiki_page_create(client, data): "project": data.private_project1.pk, }) results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({ "content": "test", @@ -287,11 +287,11 @@ def test_wiki_page_patch(client, data): with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version}) results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) @@ -361,13 +361,13 @@ def test_wiki_link_update(client, data): wiki_link_data["title"] = "test" wiki_link_data = json.dumps(wiki_link_data) results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data wiki_link_data["title"] = "test" wiki_link_data = json.dumps(wiki_link_data) results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users) - assert results == [401, 200, 200, 200, 200] + assert results == [401, 403, 403, 200, 200] wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data wiki_link_data["title"] = "test" @@ -450,7 +450,7 @@ def test_wiki_link_create(client, data): "project": data.public_project.pk, }) results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({ "title": "test", @@ -458,7 +458,7 @@ def test_wiki_link_create(client, data): "project": data.private_project1.pk, }) results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 201, 201, 201, 201] + assert results == [401, 403, 403, 201, 201] create_data = json.dumps({ "title": "test", @@ -492,21 +492,21 @@ def test_wiki_link_patch(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 200, 200, 200, 200] + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 200, 200, 200, 200] + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] def test_wikipage_action_watch(client, data): diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 4e9da6e5..7ca9b867 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -14,7 +14,7 @@ from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.users import models from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer from taiga.auth.tokens import get_token_for_user -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.users.services import get_watched_list, get_voted_list, get_liked_list from taiga.projects.notifications.choices import NotifyLevel @@ -340,7 +340,7 @@ def test_list_contacts_no_projects(client): def test_list_contacts_public_projects(client): project = f.ProjectFactory.create(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS))) + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) user_1 = f.UserFactory.create() user_2 = f.UserFactory.create() diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py index 2608864b..6c4f7d41 100644 --- a/tests/integration/test_watch_projects.py +++ b/tests/integration/test_watch_projects.py @@ -20,7 +20,7 @@ import pytest import json from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS +from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from .. import factories as f @@ -129,7 +129,7 @@ def test_get_project_is_watcher(client): user = f.UserFactory.create() project = f.ProjectFactory.create(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS))) + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS))) url_detail = reverse("projects-detail", args=(project.id,)) url_watch = reverse("projects-watch", args=(project.id,)) From 38e5198cc9443fb1d6c1c67a99d7c5dfafee6f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 23 May 2016 14:57:52 +0200 Subject: [PATCH 022/261] Minor refactor over permissions module --- ...rate_fixtures_initial_project_templates.sh | 6 ++ taiga/base/api/permissions.py | 33 ++------ taiga/permissions/choices.py | 72 ++++++++++++++++ taiga/permissions/permissions.py | 83 +++++++------------ taiga/permissions/{service.py => services.py} | 9 +- taiga/projects/api.py | 12 +-- taiga/projects/history/permissions.py | 2 +- taiga/projects/issues/permissions.py | 14 +--- .../management/commands/sample_data.py | 2 +- taiga/projects/models.py | 11 ++- taiga/projects/notifications/services.py | 2 +- taiga/projects/permissions.py | 25 +++--- taiga/projects/references/api.py | 2 +- taiga/projects/serializers.py | 4 +- taiga/projects/signals.py | 1 - taiga/projects/tasks/permissions.py | 5 +- taiga/projects/userstories/api.py | 1 - taiga/projects/userstories/permissions.py | 7 +- taiga/searches/api.py | 2 +- taiga/timeline/permissions.py | 8 +- taiga/users/models.py | 2 +- taiga/webhooks/permissions.py | 2 +- tests/factories.py | 2 +- .../test_attachment_resources.py | 2 +- .../test_history_resources.py | 2 +- .../test_issues_custom_attributes_resource.py | 2 +- .../test_issues_resources.py | 2 +- .../test_milestones_resources.py | 2 +- .../test_modules_resources.py | 2 +- .../test_projects_choices_resources.py | 2 +- .../test_projects_resource.py | 2 +- .../test_resolver_resources.py | 2 +- .../test_search_resources.py | 2 +- .../test_tasks_custom_attributes_resource.py | 2 +- .../test_tasks_resources.py | 2 +- .../test_timelines_resources.py | 2 +- ..._userstories_custom_attributes_resource.py | 2 +- .../test_userstories_resources.py | 2 +- .../test_wiki_resources.py | 2 +- tests/integration/test_notifications.py | 2 +- tests/integration/test_permissions.py | 36 ++++---- tests/integration/test_projects.py | 2 +- tests/integration/test_searches.py | 2 +- tests/integration/test_users.py | 2 +- tests/integration/test_watch_projects.py | 2 +- tests/unit/test_permissions.py | 26 ------ 46 files changed, 205 insertions(+), 206 deletions(-) create mode 100755 scripts/generate_fixtures_initial_project_templates.sh create mode 100644 taiga/permissions/choices.py rename taiga/permissions/{service.py => services.py} (96%) delete mode 100644 tests/unit/test_permissions.py diff --git a/scripts/generate_fixtures_initial_project_templates.sh b/scripts/generate_fixtures_initial_project_templates.sh new file mode 100755 index 00000000..d0201489 --- /dev/null +++ b/scripts/generate_fixtures_initial_project_templates.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +python ./manage.py dumpdata --format json \ + --indent 4 \ + --output './taiga/projects/fixtures/initial_project_templates.json' \ + 'projects.ProjectTemplate' diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py index 62b40619..19b366fc 100644 --- a/taiga/base/api/permissions.py +++ b/taiga/base/api/permissions.py @@ -20,11 +20,12 @@ import abc from functools import reduce from taiga.base.utils import sequence as sq -from taiga.permissions.service import user_has_perm, is_project_admin +from taiga.permissions.services import user_has_perm, is_project_admin from django.apps import apps from django.utils.translation import ugettext as _ + ###################################################################### # Base permissiones definition ###################################################################### @@ -179,33 +180,6 @@ class HasProjectPerm(PermissionComponent): return user_has_perm(request.user, self.project_perm, obj) -class HasProjectParamAndPerm(PermissionComponent): - def __init__(self, perm, *components): - self.project_perm = perm - super().__init__(*components) - - def check_permissions(self, request, view, obj=None): - Project = apps.get_model('projects', 'Project') - project_id = request.QUERY_PARAMS.get("project", None) - try: - project = Project.objects.get(pk=project_id) - except Project.DoesNotExist: - return False - return user_has_perm(request.user, self.project_perm, project) - - -class HasMandatoryParam(PermissionComponent): - def __init__(self, param, *components): - self.mandatory_param = param - super().__init__(*components) - - def check_permissions(self, request, view, obj=None): - param = request.GET.get(self.mandatory_param, None) - if param: - return True - return False - - class IsProjectAdmin(PermissionComponent): def check_permissions(self, request, view, obj=None): return is_project_admin(request.user, obj) @@ -213,6 +187,9 @@ class IsProjectAdmin(PermissionComponent): class IsObjectOwner(PermissionComponent): def check_permissions(self, request, view, obj=None): + if obj.owner is None: + return False + return obj.owner == request.user diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py new file mode 100644 index 00000000..bfd7192e --- /dev/null +++ b/taiga/permissions/choices.py @@ -0,0 +1,72 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 _ + +ANON_PERMISSIONS = [ + ('view_project', _('View project')), + ('view_milestones', _('View milestones')), + ('view_us', _('View user stories')), + ('view_tasks', _('View tasks')), + ('view_issues', _('View issues')), + ('view_wiki_pages', _('View wiki pages')), + ('view_wiki_links', _('View wiki links')), +] + +MEMBERS_PERMISSIONS = [ + ('view_project', _('View project')), + # Milestone permissions + ('view_milestones', _('View milestones')), + ('add_milestone', _('Add milestone')), + ('modify_milestone', _('Modify milestone')), + ('delete_milestone', _('Delete milestone')), + # US permissions + ('view_us', _('View user story')), + ('add_us', _('Add user story')), + ('modify_us', _('Modify user story')), + ('delete_us', _('Delete user story')), + # Task permissions + ('view_tasks', _('View tasks')), + ('add_task', _('Add task')), + ('modify_task', _('Modify task')), + ('delete_task', _('Delete task')), + # Issue permissions + ('view_issues', _('View issues')), + ('add_issue', _('Add issue')), + ('modify_issue', _('Modify issue')), + ('delete_issue', _('Delete issue')), + # Wiki page permissions + ('view_wiki_pages', _('View wiki pages')), + ('add_wiki_page', _('Add wiki page')), + ('modify_wiki_page', _('Modify wiki page')), + ('delete_wiki_page', _('Delete wiki page')), + # Wiki link permissions + ('view_wiki_links', _('View wiki links')), + ('add_wiki_link', _('Add wiki link')), + ('modify_wiki_link', _('Modify wiki link')), + ('delete_wiki_link', _('Delete wiki link')), +] + +ADMINS_PERMISSIONS = [ + ('modify_project', _('Modify project')), + ('delete_project', _('Delete project')), + ('add_member', _('Add member')), + ('remove_member', _('Remove member')), + ('admin_project_values', _('Admin project values')), + ('admin_roles', _('Admin roles')), +] diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py index 61c0b683..4e563522 100644 --- a/taiga/permissions/permissions.py +++ b/taiga/permissions/permissions.py @@ -16,57 +16,38 @@ # 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.apps import apps -ANON_PERMISSIONS = [ - ('view_project', _('View project')), - ('view_milestones', _('View milestones')), - ('view_us', _('View user stories')), - ('view_tasks', _('View tasks')), - ('view_issues', _('View issues')), - ('view_wiki_pages', _('View wiki pages')), - ('view_wiki_links', _('View wiki links')), -] +from taiga.base.api.permissions import PermissionComponent -MEMBERS_PERMISSIONS = [ - ('view_project', _('View project')), - # Milestone permissions - ('view_milestones', _('View milestones')), - ('add_milestone', _('Add milestone')), - ('modify_milestone', _('Modify milestone')), - ('delete_milestone', _('Delete milestone')), - # US permissions - ('view_us', _('View user story')), - ('add_us', _('Add user story')), - ('modify_us', _('Modify user story')), - ('delete_us', _('Delete user story')), - # Task permissions - ('view_tasks', _('View tasks')), - ('add_task', _('Add task')), - ('modify_task', _('Modify task')), - ('delete_task', _('Delete task')), - # Issue permissions - ('view_issues', _('View issues')), - ('add_issue', _('Add issue')), - ('modify_issue', _('Modify issue')), - ('delete_issue', _('Delete issue')), - # Wiki page permissions - ('view_wiki_pages', _('View wiki pages')), - ('add_wiki_page', _('Add wiki page')), - ('modify_wiki_page', _('Modify wiki page')), - ('delete_wiki_page', _('Delete wiki page')), - # Wiki link permissions - ('view_wiki_links', _('View wiki links')), - ('add_wiki_link', _('Add wiki link')), - ('modify_wiki_link', _('Modify wiki link')), - ('delete_wiki_link', _('Delete wiki link')), -] +from . import services -ADMINS_PERMISSIONS = [ - ('modify_project', _('Modify project')), - ('add_member', _('Add member')), - ('remove_member', _('Remove member')), - ('delete_project', _('Delete project')), - ('admin_project_values', _('Admin project values')), - ('admin_roles', _('Admin roles')), -] + +###################################################################### +# Generic perms +###################################################################### + +class HasProjectPerm(PermissionComponent): + def __init__(self, perm, *components): + self.project_perm = perm + super().__init__(*components) + + def check_permissions(self, request, view, obj=None): + return services.user_has_perm(request.user, self.project_perm, obj) + + +class IsObjectOwner(PermissionComponent): + def check_permissions(self, request, view, obj=None): + if obj.owner is None: + return False + + return obj.owner == request.user + + +###################################################################### +# Project Perms +###################################################################### + +class IsProjectAdmin(PermissionComponent): + def check_permissions(self, request, view, obj=None): + return services.is_project_admin(request.user, obj) diff --git a/taiga/permissions/service.py b/taiga/permissions/services.py similarity index 96% rename from taiga/permissions/service.py rename to taiga/permissions/services.py index 02922574..9ed1e8c3 100644 --- a/taiga/permissions/service.py +++ b/taiga/permissions/services.py @@ -16,10 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from .permissions import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from .choices import ADMINS_PERMISSIONS, MEMBERS_PERMISSIONS, ANON_PERMISSIONS from django.apps import apps + def _get_user_project_membership(user, project, cache="user"): """ cache param determines how memberships are calculated trying to reuse the existing data @@ -83,10 +84,6 @@ def user_has_perm(user, perm, obj=None, cache="user"): return perm in get_user_project_permissions(user, project, cache=cache) -def role_has_perm(role, perm): - return perm in role.permissions - - def _get_membership_permissions(membership): if membership and membership.role and membership.role.permissions: return membership.role.permissions @@ -97,7 +94,7 @@ def get_user_project_permissions(user, project, cache="user"): """ cache param determines how memberships are calculated trying to reuse the existing data in cache - """ + """ membership = _get_user_project_membership(user, project, cache=cache) if user.is_superuser: admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index a7c3aa61..17684fd3 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -51,8 +51,8 @@ from taiga.projects.userstories.models import UserStory, RolePoints from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin -from taiga.permissions import service as permissions_service -from taiga.users import services as users_service +from taiga.permissions import services as permissions_services +from taiga.users import services as users_services from . import filters as project_filters from . import models @@ -147,7 +147,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, else: project = self.get_object() - if permissions_service.is_project_admin(self.request.user, project): + if permissions_services.is_project_admin(self.request.user, project): serializer_class = self.admin_serializer_class return serializer_class @@ -415,7 +415,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, update_permissions = True if update_permissions: - permissions_service.set_base_permissions_for_project(obj) + permissions_services.set_base_permissions_for_project(obj) def pre_save(self, obj): if not obj.id: @@ -603,12 +603,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): use_admin_serializer = True if self.action == "retrieve": - use_admin_serializer = permissions_service.is_project_admin(self.request.user, self.object.project) + use_admin_serializer = permissions_services.is_project_admin(self.request.user, self.object.project) project_id = self.request.QUERY_PARAMS.get("project", None) if self.action == "list" and project_id is not None: project = get_object_or_404(models.Project, pk=project_id) - use_admin_serializer = permissions_service.is_project_admin(self.request.user, project) + use_admin_serializer = permissions_services.is_project_admin(self.request.user, project) if use_admin_serializer: return self.admin_serializer_class diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index fdce68cd..54fc59f1 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -19,7 +19,7 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, IsProjectAdmin, AllowAny, IsObjectOwner, PermissionComponent) -from taiga.permissions.service import is_project_admin +from taiga.permissions.services import is_project_admin from taiga.projects.history.services import get_model_from_key, get_pk_from_key diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index 291dede7..791848f9 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -16,9 +16,9 @@ # along with this program. If not, see . -from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsProjectAdmin, PermissionComponent, - AllowAny, IsAuthenticated, IsSuperUser) +from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser +from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin + class IssuePermission(TaigaResourcePermission): @@ -40,14 +40,6 @@ class IssuePermission(TaigaResourcePermission): unwatch_perms = IsAuthenticated() & HasProjectPerm('view_issues') -class HasIssueIdUrlParam(PermissionComponent): - def check_permissions(self, request, view, obj=None): - param = view.kwargs.get('issue_id', None) - if param: - return True - return False - - class IssueVotersPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index f5c7b6ea..be43df8c 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -29,7 +29,7 @@ from django.contrib.contenttypes.models import ContentType from sampledatahelper.helper import SampleDataHelper from taiga.users.models import * -from taiga.permissions.permissions import ANON_PERMISSIONS +from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.choices import BLOCKED_BY_STAFF from taiga.projects.models import * from taiga.projects.milestones.models import * diff --git a/taiga/projects/models.py b/taiga/projects/models.py index cd38c35b..506c2736 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -40,7 +40,7 @@ from taiga.base.utils.sequence import arithmetic_progression from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.slug import slugify_uniquely_for_queryset -from taiga.permissions.permissions import ANON_PERMISSIONS, MEMBERS_PERMISSIONS +from taiga.permissions.choices import ANON_PERMISSIONS, MEMBERS_PERMISSIONS from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.services import ( @@ -366,7 +366,8 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): @cached_property def cached_memberships(self): - return {m.user.id: m for m in self.memberships.exclude(user__isnull=True).select_related("user", "project", "role")} + return {m.user.id: m for m in self.memberships.exclude(user__isnull=True) + .select_related("user", "project", "role")} def cached_memberships_for_user(self, user): return self.cached_memberships.get(user.id, None) @@ -966,9 +967,11 @@ class ProjectTemplate(models.Model): project=project) if self.priorities: - project.default_priority = Priority.objects.get(name=self.default_options["priority"], project=project) + project.default_priority = Priority.objects.get(name=self.default_options["priority"], + project=project) if self.severities: - project.default_severity = Severity.objects.get(name=self.default_options["severity"], project=project) + project.default_severity = Severity.objects.get(name=self.default_options["severity"], + project=project) return project diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 22d518cb..27e4b5ae 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -35,7 +35,7 @@ from taiga.projects.history.choices import HistoryType from taiga.projects.history.services import (make_key_from_model_object, get_last_snapshot_for_key, get_model_from_key) -from taiga.permissions.service import user_has_perm +from taiga.permissions.services import user_has_perm from .models import HistoryChangeNotification, Watched diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index de65cd69..ba4f3260 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -18,18 +18,21 @@ from django.utils.translation import ugettext as _ from taiga.base.api.permissions import TaigaResourcePermission -from taiga.base.api.permissions import HasProjectPerm from taiga.base.api.permissions import IsAuthenticated -from taiga.base.api.permissions import IsProjectAdmin from taiga.base.api.permissions import AllowAny from taiga.base.api.permissions import IsSuperUser +from taiga.base.api.permissions import IsObjectOwner from taiga.base.api.permissions import PermissionComponent from taiga.base import exceptions as exc -from taiga.projects.models import Membership +from taiga.permissions.permissions import HasProjectPerm +from taiga.permissions.permissions import IsProjectAdmin + +from . import models from . import services + class CanLeaveProject(PermissionComponent): def check_permissions(self, request, view, obj=None): if not obj or not request.user.is_authenticated(): @@ -37,20 +40,12 @@ class CanLeaveProject(PermissionComponent): try: if not services.can_user_leave_project(request.user, obj): - raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are no more admins")) + raise exc.PermissionDenied(_("You can't leave the project if you are the owner or there are " + "no more admins")) return True - except Membership.DoesNotExist: + except models.Membership.DoesNotExist: return False -class IsMainOwner(PermissionComponent): - def check_permissions(self, request, view, obj=None): - if not obj or not request.user.is_authenticated(): - return False - - if obj.owner is None: - return False - - return obj.owner == request.user class ProjectPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') @@ -79,7 +74,7 @@ class ProjectPermission(TaigaResourcePermission): leave_perms = CanLeaveProject() transfer_validate_token_perms = IsAuthenticated() & HasProjectPerm('view_project') transfer_request_perms = IsProjectAdmin() - transfer_start_perms = IsMainOwner() + transfer_start_perms = IsObjectOwner() transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project') transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project') diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 028d4642..0775bc72 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -21,7 +21,7 @@ from taiga.base import exceptions as exc from taiga.base import response from taiga.base.api import viewsets from taiga.base.api.utils import get_object_or_404 -from taiga.permissions.service import user_has_perm +from taiga.permissions.services import user_has_perm from .serializers import ResolverSerializer from . import permissions diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 661aeece..7096919a 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -32,8 +32,8 @@ from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import ProjectRoleSerializer from taiga.users.validators import RoleExistsValidator -from taiga.permissions.service import get_user_project_permissions -from taiga.permissions.service import is_project_admin, is_project_owner +from taiga.permissions.services import get_user_project_permissions +from taiga.permissions.services import is_project_admin, is_project_owner from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin from . import models diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py index 51ff6485..32da1176 100644 --- a/taiga/projects/signals.py +++ b/taiga/projects/signals.py @@ -67,7 +67,6 @@ def project_post_save(sender, instance, created, **kwargs): if instance._importing: return - template = getattr(instance, "creation_template", None) if template is None: ProjectTemplate = apps.get_model("projects", "ProjectTemplate") diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 9e189f10..566fc79c 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -15,9 +15,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectAdmin, AllowAny, - IsSuperUser) +from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser +from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin class TaskPermission(TaigaResourcePermission): diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 815560c1..745268cd 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -112,7 +112,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return super().update(request, *args, **kwargs) - def get_queryset(self): qs = super().get_queryset() qs = qs.prefetch_related("role_points", diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index 326c99fe..11aa5b73 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -15,12 +15,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - IsAuthenticated, IsProjectAdmin, - AllowAny, IsSuperUser) +from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser +from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin class UserStoryPermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None retrieve_perms = HasProjectPerm('view_us') create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us') update_perms = HasProjectPerm('modify_us') diff --git a/taiga/searches/api.py b/taiga/searches/api.py index be8a0e9a..f10554a8 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -21,7 +21,7 @@ from taiga.base.api import viewsets from taiga.base import response from taiga.base.api.utils import get_object_or_404 -from taiga.permissions.service import user_has_perm +from taiga.permissions.services import user_has_perm from . import services from . import serializers diff --git a/taiga/timeline/permissions.py b/taiga/timeline/permissions.py index b71530ed..f2753869 100644 --- a/taiga/timeline/permissions.py +++ b/taiga/timeline/permissions.py @@ -15,13 +15,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, - AllowAny) +from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsSuperUser +from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin class UserTimelinePermission(TaigaResourcePermission): + enought_perms = IsSuperUser() + global_perms = None retrieve_perms = AllowAny() class ProjectTimelinePermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None retrieve_perms = HasProjectPerm('view_project') diff --git a/taiga/users/models.py b/taiga/users/models.py index a0a9048b..c1cf4a3b 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -38,7 +38,7 @@ from djorm_pgarray.fields import TextArrayField from taiga.auth.tokens import get_token_for_user from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.files import get_file_path -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING from taiga.projects.notifications.choices import NotifyLevel diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py index bc1cae61..82ca5bd7 100644 --- a/taiga/webhooks/permissions.py +++ b/taiga/webhooks/permissions.py @@ -18,7 +18,7 @@ from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectAdmin, AllowAny, PermissionComponent) -from taiga.permissions.service import is_project_admin +from taiga.permissions.services import is_project_admin class IsWebhookProjectAdmin(PermissionComponent): diff --git a/tests/factories.py b/tests/factories.py index 252ce47a..2f1ddccc 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -26,7 +26,7 @@ from .utils import DUMMY_BMP_DATA import factory -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py index c48a7e3a..357b13a7 100644 --- a/tests/integration/resources_permissions/test_attachment_resources.py +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -4,7 +4,7 @@ from django.test.client import MULTIPART_CONTENT from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.projects.attachments.serializers import AttachmentSerializer diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index 9d39b9d7..62ba4e77 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse from django.utils import timezone from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.history.models import HistoryEntry from taiga.projects.history.choices import HistoryType from taiga.projects.history.services import make_key_from_model_object diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 718172df..60b57eed 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, ANON_PERMISSIONS) from tests import factories as f diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 4cf3e2a1..bb532473 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse from taiga.projects import choices as project_choices from taiga.projects.issues.serializers import IssueSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json from tests import factories as f diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index c42c0580..56f072f4 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -6,7 +6,7 @@ from taiga.projects import choices as project_choices from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone from taiga.projects.notifications.services import add_watcher -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py index 27269458..fc1de642 100644 --- a/tests/integration/resources_permissions/test_modules_resources.py +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -2,7 +2,7 @@ import uuid from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json from tests import factories as f diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index 207889f9..c26e51b5 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -4,7 +4,7 @@ from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects import serializers from taiga.users.serializers import RoleSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 9bcd7a9f..a2ec130e 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -4,7 +4,7 @@ from django.apps import apps from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.serializers import ProjectDetailSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, helper_test_http_method_and_count diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index 6858d976..c8634c46 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index 783818d3..d72e7bc2 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method_and_keys, disconnect_signals, reconnect_signals diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index 44509354..2262a222 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, ANON_PERMISSIONS) from tests import factories as f diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 274f7a0c..aff81389 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.tasks.serializers import TaskSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py index ffc4e41d..ed68f67b 100644 --- a/tests/integration/resources_permissions/test_timelines_resources.py +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from tests import factories as f from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index 030060f3..64e7324a 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.custom_attributes import serializers -from taiga.permissions.permissions import (MEMBERS_PERMISSIONS, +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, ANON_PERMISSIONS) diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 0daadad5..4da7b081 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices from taiga.projects.userstories.serializers import UserStorySerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index ed76e769..aaee4e53 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -1,7 +1,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.projects.notifications.services import add_watcher from taiga.projects.occ import OCCResourceMixin diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index bb2cdfd8..4d341086 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -41,7 +41,7 @@ from taiga.projects.history.services import take_snapshot from taiga.projects.issues.serializers import IssueSerializer from taiga.projects.userstories.serializers import UserStorySerializer from taiga.projects.tasks.serializers import TaskSerializer -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS pytestmark = pytest.mark.django_db diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py index ddcf9e34..7ec25929 100644 --- a/tests/integration/test_permissions.py +++ b/tests/integration/test_permissions.py @@ -1,6 +1,6 @@ import pytest -from taiga.permissions import service, permissions +from taiga.permissions import services, choices from django.contrib.auth.models import AnonymousUser from .. import factories @@ -15,15 +15,15 @@ def test_get_user_project_role(): role = factories.RoleFactory() membership = factories.MembershipFactory(user=user1, project=project, role=role) - assert service._get_user_project_membership(user1, project) == membership - assert service._get_user_project_membership(user2, project) is None + assert services._get_user_project_membership(user1, project) == membership + assert services._get_user_project_membership(user2, project) is None def test_anon_get_user_project_permissions(): project = factories.ProjectFactory() project.anon_permissions = ["test1"] project.public_permissions = ["test2"] - assert service.get_user_project_permissions(AnonymousUser(), project) == set(["test1"]) + assert services.get_user_project_permissions(AnonymousUser(), project) == set(["test1"]) def test_user_get_user_project_permissions_on_public_project(): @@ -31,7 +31,7 @@ def test_user_get_user_project_permissions_on_public_project(): project = factories.ProjectFactory() project.anon_permissions = ["test1"] project.public_permissions = ["test2"] - assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"]) + assert services.get_user_project_permissions(user1, project) == set(["test1", "test2"]) def test_user_get_user_project_permissions_on_private_project(): @@ -40,7 +40,7 @@ def test_user_get_user_project_permissions_on_private_project(): project.anon_permissions = ["test1"] project.public_permissions = ["test2"] project.is_private = True - assert service.get_user_project_permissions(user1, project) == set(["test1", "test2"]) + assert services.get_user_project_permissions(user1, project) == set(["test1", "test2"]) def test_owner_get_user_project_permissions(): @@ -55,7 +55,7 @@ def test_owner_get_user_project_permissions(): expected_perms = set( ["test1", "test2", "view_us"] ) - assert service.get_user_project_permissions(user1, project) == expected_perms + assert services.get_user_project_permissions(user1, project) == expected_perms def test_owner_member_get_user_project_permissions(): @@ -68,10 +68,10 @@ def test_owner_member_get_user_project_permissions(): expected_perms = set( ["test1", "test2", "test3"] + - [x[0] for x in permissions.ADMINS_PERMISSIONS] + - [x[0] for x in permissions.MEMBERS_PERMISSIONS] + [x[0] for x in choices.ADMINS_PERMISSIONS] + + [x[0] for x in choices.MEMBERS_PERMISSIONS] ) - assert service.get_user_project_permissions(user1, project) == expected_perms + assert services.get_user_project_permissions(user1, project) == expected_perms def test_member_get_user_project_permissions(): @@ -82,22 +82,22 @@ def test_member_get_user_project_permissions(): role = factories.RoleFactory(permissions=["test3"]) factories.MembershipFactory(user=user1, project=project, role=role) - assert service.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"]) + assert services.get_user_project_permissions(user1, project) == set(["test1", "test2", "test3"]) def test_anon_user_has_perm(): project = factories.ProjectFactory() project.anon_permissions = ["test"] - assert service.user_has_perm(AnonymousUser(), "test", project) is True - assert service.user_has_perm(AnonymousUser(), "fail", project) is False + assert services.user_has_perm(AnonymousUser(), "test", project) is True + assert services.user_has_perm(AnonymousUser(), "fail", project) is False def test_authenticated_user_has_perm_on_project(): user1 = factories.UserFactory() project = factories.ProjectFactory() project.public_permissions = ["test"] - assert service.user_has_perm(user1, "test", project) is True - assert service.user_has_perm(user1, "fail", project) is False + assert services.user_has_perm(user1, "test", project) is True + assert services.user_has_perm(user1, "fail", project) is False def test_authenticated_user_has_perm_on_project_related_object(): @@ -106,10 +106,10 @@ def test_authenticated_user_has_perm_on_project_related_object(): project.public_permissions = ["test"] us = factories.UserStoryFactory(project=project) - assert service.user_has_perm(user1, "test", us) is True - assert service.user_has_perm(user1, "fail", us) is False + assert services.user_has_perm(user1, "test", us) is True + assert services.user_has_perm(user1, "fail", us) is False def test_authenticated_user_has_perm_on_invalid_object(): user1 = factories.UserFactory() - assert service.user_has_perm(user1, "test", user1) is False + assert services.user_has_perm(user1, "test", user1) is False diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 6111747b..28a0dbe3 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -7,7 +7,7 @@ from django.core import signing from taiga.base.utils import json from taiga.projects.services import stats as stats_services from taiga.projects.history.services import take_snapshot -from taiga.permissions.permissions import ANON_PERMISSIONS +from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.models import Project from .. import factories as f diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index 334ad405..30c3247b 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -22,7 +22,7 @@ from django.core.urlresolvers import reverse from .. import factories as f -from taiga.permissions.permissions import MEMBERS_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS from tests.utils import disconnect_signals, reconnect_signals diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 7ca9b867..be48c553 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -14,7 +14,7 @@ from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.users import models from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer from taiga.auth.tokens import get_token_for_user -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects import choices as project_choices from taiga.users.services import get_watched_list, get_voted_list, get_liked_list from taiga.projects.notifications.choices import NotifyLevel diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py index 6c4f7d41..5a77f086 100644 --- a/tests/integration/test_watch_projects.py +++ b/tests/integration/test_watch_projects.py @@ -20,7 +20,7 @@ import pytest import json from django.core.urlresolvers import reverse -from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from .. import factories as f diff --git a/tests/unit/test_permissions.py b/tests/unit/test_permissions.py deleted file mode 100644 index 5ef7a93d..00000000 --- a/tests/unit/test_permissions.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# 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 taiga.permissions import service -from taiga.users.models import Role - - -def test_role_has_perm(): - role = Role() - role.permissions = ["test"] - assert service.role_has_perm(role, "test") - assert service.role_has_perm(role, "false") is False From 413a83808a5c1836270928707faa6bcd75b5dffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 11 May 2016 20:50:19 +0200 Subject: [PATCH 023/261] US #4186: Permission to add comments --- CHANGELOG.md | 7 +- taiga/permissions/choices.py | 5 +- taiga/permissions/permissions.py | 37 + .../fixtures/initial_project_templates.json | 84 +- taiga/projects/issues/permissions.py | 5 +- .../migrations/0041_auto_20160519_1058.py | 21 + .../migrations/0042_auto_20160525_0911.py | 67 ++ taiga/projects/tasks/permissions.py | 6 +- taiga/projects/userstories/permissions.py | 6 +- taiga/projects/wiki/permissions.py | 6 +- .../migrations/0019_auto_20160519_1058.py | 21 + .../migrations/0020_auto_20160525_1229.py | 48 ++ .../test_issues_resources.py | 532 ++++++++---- .../test_tasks_resources.py | 466 +++++++---- .../test_userstories_resources.py | 380 ++++++--- .../test_wiki_resources.py | 786 ++++++++++++------ 16 files changed, 1720 insertions(+), 757 deletions(-) create mode 100644 taiga/projects/migrations/0041_auto_20160519_1058.py create mode 100644 taiga/projects/migrations/0042_auto_20160525_0911.py create mode 100644 taiga/users/migrations/0019_auto_20160519_1058.py create mode 100644 taiga/users/migrations/0020_auto_20160525_1229.py diff --git a/CHANGELOG.md b/CHANGELOG.md index eca3114a..97b090ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Changelog # ## 2.2.0 ??? (unreleased) + ### Features -- [API] edit comment endpoint: comment owners and project admins can edit existing comments +- Now comment owners and project admins can edit existing comments with the history Entry endpoint. +- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. + +### Misc +- Lots of small and not so small bugfixes. ## 2.1.0 Ursus Americanus (2016-05-03) diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py index bfd7192e..f617da97 100644 --- a/taiga/permissions/choices.py +++ b/taiga/permissions/choices.py @@ -27,7 +27,6 @@ ANON_PERMISSIONS = [ ('view_wiki_pages', _('View wiki pages')), ('view_wiki_links', _('View wiki links')), ] - MEMBERS_PERMISSIONS = [ ('view_project', _('View project')), # Milestone permissions @@ -39,21 +38,25 @@ MEMBERS_PERMISSIONS = [ ('view_us', _('View user story')), ('add_us', _('Add user story')), ('modify_us', _('Modify user story')), + ('comment_us', _('Comment user story')), ('delete_us', _('Delete user story')), # Task permissions ('view_tasks', _('View tasks')), ('add_task', _('Add task')), ('modify_task', _('Modify task')), + ('comment_task', _('Comment task')), ('delete_task', _('Delete task')), # Issue permissions ('view_issues', _('View issues')), ('add_issue', _('Add issue')), ('modify_issue', _('Modify issue')), + ('comment_issue', _('Comment issue')), ('delete_issue', _('Delete issue')), # Wiki page permissions ('view_wiki_pages', _('View wiki pages')), ('add_wiki_page', _('Add wiki page')), ('modify_wiki_page', _('Modify wiki page')), + ('comment_wiki_page', _('Comment wiki page')), ('delete_wiki_page', _('Delete wiki page')), # Wiki link permissions ('view_wiki_links', _('View wiki links')), diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py index 4e563522..c2b7699a 100644 --- a/taiga/permissions/permissions.py +++ b/taiga/permissions/permissions.py @@ -51,3 +51,40 @@ class IsObjectOwner(PermissionComponent): class IsProjectAdmin(PermissionComponent): def check_permissions(self, request, view, obj=None): return services.is_project_admin(request.user, obj) + + +###################################################################### +# Common perms for stories, tasks and issues +###################################################################### + +class CommentAndOrUpdatePerm(PermissionComponent): + def __init__(self, update_perm, comment_perm, *components): + self.update_perm = update_perm + self.comment_perm = comment_perm + super().__init__(*components) + + def check_permissions(self, request, view, obj=None): + if not obj: + return False + + project_id = request.DATA.get('project', None) + if project_id and obj.project_id != project_id: + project = apps.get_model("projects", "Project").objects.get(pk=project_id) + else: + project = obj.project + + data_keys = request.DATA.keys() + + if (not services.user_has_perm(request.user, self.comment_perm, project) and + "comment" in data_keys): + # User can't comment but there is a comment in the request + #raise exc.PermissionDenied(_("You don't have permissions to comment this.")) + return False + + if (not services.user_has_perm(request.user, self.update_perm, project) and + len(data_keys - "comment")): + # User can't update but there is a change in the request + #raise exc.PermissionDenied(_("You don't have permissions to update this.")) + return False + + return True diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index 46d369a5..54e73bd0 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -1,56 +1,56 @@ [ { "model": "projects.projecttemplate", + "pk": 1, "fields": { - "is_issues_activated": true, - "task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff9900\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#ffcc00\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#999999\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]", - "is_backlog_activated": true, - "modified_date": "2014-07-25T10:02:46.479Z", - "us_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#ff8a84\", \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\"}, {\"color\": \"#ff9900\", \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#fcc000\", \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#669900\", \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\"}, {\"color\": \"#5c3566\", \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\"}]", - "is_wiki_activated": true, - "roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]", - "points": "[{\"value\": null, \"order\": 1, \"name\": \"?\"}, {\"value\": 0.0, \"order\": 2, \"name\": \"0\"}, {\"value\": 0.5, \"order\": 3, \"name\": \"1/2\"}, {\"value\": 1.0, \"order\": 4, \"name\": \"1\"}, {\"value\": 2.0, \"order\": 5, \"name\": \"2\"}, {\"value\": 3.0, \"order\": 6, \"name\": \"3\"}, {\"value\": 5.0, \"order\": 7, \"name\": \"5\"}, {\"value\": 8.0, \"order\": 8, \"name\": \"8\"}, {\"value\": 10.0, \"order\": 9, \"name\": \"10\"}, {\"value\": 13.0, \"order\": 10, \"name\": \"13\"}, {\"value\": 20.0, \"order\": 11, \"name\": \"20\"}, {\"value\": 40.0, \"order\": 12, \"name\": \"40\"}]", - "severities": "[{\"color\": \"#666666\", \"order\": 1, \"name\": \"Wishlist\"}, {\"color\": \"#669933\", \"order\": 2, \"name\": \"Minor\"}, {\"color\": \"#0000FF\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#FFA500\", \"order\": 4, \"name\": \"Important\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"Critical\"}]", - "is_kanban_activated": false, - "priorities": "[{\"color\": \"#666666\", \"order\": 1, \"name\": \"Low\"}, {\"color\": \"#669933\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"High\"}]", - "created_date": "2014-04-22T14:48:43.596Z", - "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}", + "name": "Scrum", "slug": "scrum", - "videoconferences_extra_data": "", - "issue_statuses": "[{\"color\": \"#8C2318\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#5E8C6A\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#88A65E\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#BFB35A\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#89BAB4\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#CC0000\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#666666\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]", - "default_owner_role": "product-owner", - "issue_types": "[{\"color\": \"#89BAB4\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#ba89a8\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#89a8ba\", \"order\": 3, \"name\": \"Enhancement\"}]", - "videoconferences": null, "description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers", - "name": "Scrum" - }, - "pk": 1 + "created_date": "2014-04-22T14:48:43.596Z", + "modified_date": "2014-07-25T10:02:46.479Z", + "default_owner_role": "product-owner", + "is_backlog_activated": true, + "is_kanban_activated": false, + "is_wiki_activated": true, + "is_issues_activated": true, + "videoconferences": null, + "videoconferences_extra_data": "", + "default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}", + "us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#669900\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]", + "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]", + "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#ff9900\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#ffcc00\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#669900\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#999999\", \"is_closed\": false}]", + "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#8C2318\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#5E8C6A\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#88A65E\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#BFB35A\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#89BAB4\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#CC0000\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#666666\", \"is_closed\": false}]", + "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#89BAB4\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#ba89a8\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#89a8ba\"}]", + "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#666666\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#669933\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]", + "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#666666\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#669933\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#0000FF\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#FFA500\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]", + "roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]" + } }, { "model": "projects.projecttemplate", + "pk": 2, "fields": { - "is_issues_activated": false, - "task_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}]", - "is_backlog_activated": false, - "modified_date": "2014-07-25T13:11:42.754Z", - "us_statuses": "[{\"wip_limit\": null, \"order\": 1, \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"name\": \"New\", \"slug\": \"new\"}, {\"wip_limit\": null, \"order\": 2, \"is_closed\": false, \"is_archived\": false, \"color\": \"#f57900\", \"name\": \"Ready\", \"slug\": \"ready\"}, {\"wip_limit\": null, \"order\": 3, \"is_closed\": false, \"is_archived\": false, \"color\": \"#729fcf\", \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"wip_limit\": null, \"order\": 4, \"is_closed\": false, \"is_archived\": false, \"color\": \"#4e9a06\", \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"wip_limit\": null, \"order\": 5, \"is_closed\": true, \"is_archived\": false, \"color\": \"#cc0000\", \"name\": \"Done\", \"slug\": \"done\"}, {\"wip_limit\": null, \"order\": 6, \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"name\": \"Archived\", \"slug\": \"archived\"}]", - "is_wiki_activated": false, - "roles": "[{\"order\": 10, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"UX\", \"computable\": true}, {\"order\": 20, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Design\", \"computable\": true}, {\"order\": 30, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Front\", \"computable\": true}, {\"order\": 40, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Back\", \"computable\": true}, {\"order\": 50, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Product Owner\", \"computable\": false}, {\"order\": 60, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"name\": \"Stakeholder\", \"computable\": false}]", - "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]", - "severities": "[{\"color\": \"#999999\", \"order\": 1, \"name\": \"Wishlist\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Minor\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#f57900\", \"order\": 4, \"name\": \"Important\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"Critical\"}]", - "is_kanban_activated": true, - "priorities": "[{\"color\": \"#999999\", \"order\": 1, \"name\": \"Low\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Normal\"}, {\"color\": \"#CC0000\", \"order\": 5, \"name\": \"High\"}]", - "created_date": "2014-04-22T14:50:19.738Z", - "default_options": "{\"us_status\": \"New\", \"task_status\": \"New\", \"priority\": \"Normal\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"points\": \"?\", \"issue_status\": \"New\"}", + "name": "Kanban", "slug": "kanban", - "videoconferences_extra_data": "", - "issue_statuses": "[{\"color\": \"#999999\", \"order\": 1, \"is_closed\": false, \"name\": \"New\", \"slug\": \"new\"}, {\"color\": \"#729fcf\", \"order\": 2, \"is_closed\": false, \"name\": \"In progress\", \"slug\": \"in-progress\"}, {\"color\": \"#f57900\", \"order\": 3, \"is_closed\": true, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\"}, {\"color\": \"#4e9a06\", \"order\": 4, \"is_closed\": true, \"name\": \"Closed\", \"slug\": \"closed\"}, {\"color\": \"#cc0000\", \"order\": 5, \"is_closed\": false, \"name\": \"Needs Info\", \"slug\": \"needs-info\"}, {\"color\": \"#d3d7cf\", \"order\": 6, \"is_closed\": true, \"name\": \"Rejected\", \"slug\": \"rejected\"}, {\"color\": \"#75507b\", \"order\": 7, \"is_closed\": false, \"name\": \"Postponed\", \"slug\": \"posponed\"}]", - "default_owner_role": "product-owner", - "issue_types": "[{\"color\": \"#cc0000\", \"order\": 1, \"name\": \"Bug\"}, {\"color\": \"#729fcf\", \"order\": 2, \"name\": \"Question\"}, {\"color\": \"#4e9a06\", \"order\": 3, \"name\": \"Enhancement\"}]", - "videoconferences": null, "description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.", - "name": "Kanban" - }, - "pk": 2 + "created_date": "2014-04-22T14:50:19.738Z", + "modified_date": "2014-07-25T13:11:42.754Z", + "default_owner_role": "product-owner", + "is_backlog_activated": false, + "is_kanban_activated": true, + "is_wiki_activated": false, + "is_issues_activated": false, + "videoconferences": null, + "videoconferences_extra_data": "", + "default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}", + "us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#f57900\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#729fcf\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#4e9a06\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#cc0000\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]", + "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]", + "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}]", + "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#d3d7cf\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#75507b\", \"is_closed\": false}]", + "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#cc0000\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#4e9a06\"}]", + "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#999999\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]", + "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#999999\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#f57900\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]", + "roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]" + } } ] diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index 791848f9..ea823fcc 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -19,6 +19,7 @@ from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin +from taiga.permissions.permissions import CommentAndOrUpdatePerm class IssuePermission(TaigaResourcePermission): @@ -26,8 +27,8 @@ class IssuePermission(TaigaResourcePermission): global_perms = None retrieve_perms = HasProjectPerm('view_issues') create_perms = HasProjectPerm('add_issue') - update_perms = HasProjectPerm('modify_issue') - partial_update_perms = HasProjectPerm('modify_issue') + update_perms = CommentAndOrUpdatePerm('modify_issue', 'comment_issue') + partial_update_perms = CommentAndOrUpdatePerm('modify_issue', 'comment_issue') destroy_perms = HasProjectPerm('delete_issue') list_perms = AllowAny() filters_data_perms = AllowAny() diff --git a/taiga/projects/migrations/0041_auto_20160519_1058.py b/taiga/projects/migrations/0041_auto_20160519_1058.py new file mode 100644 index 00000000..c4b0a2fd --- /dev/null +++ b/taiga/projects/migrations/0041_auto_20160519_1058.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-19 10:58 +from __future__ import unicode_literals + +from django.db import migrations +import djorm_pgarray.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0040_remove_memberships_of_cancelled_users_acounts'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='public_permissions', + field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', default=[], verbose_name='user permissions'), + ), + ] diff --git a/taiga/projects/migrations/0042_auto_20160525_0911.py b/taiga/projects/migrations/0042_auto_20160525_0911.py new file mode 100644 index 00000000..0652df06 --- /dev/null +++ b/taiga/projects/migrations/0042_auto_20160525_0911.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-25 09:11 +from __future__ import unicode_literals + +from django.db import migrations + + +UPDATE_PROJECTS_ANON_PERMISSIONS_SQL = """ + UPDATE projects_project + SET + ANON_PERMISSIONS = array_append(ANON_PERMISSIONS, '{comment_permission}') + WHERE + '{base_permission}' = ANY(ANON_PERMISSIONS) + AND + NOT '{comment_permission}' = ANY(ANON_PERMISSIONS) +""" + +UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL = """ + UPDATE projects_project + SET + PUBLIC_PERMISSIONS = array_append(PUBLIC_PERMISSIONS, '{comment_permission}') + WHERE + '{base_permission}' = ANY(PUBLIC_PERMISSIONS) + AND + NOT '{comment_permission}' = ANY(PUBLIC_PERMISSIONS) +""" + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0041_auto_20160519_1058'), + ] + + operations = [ + # user stories + migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format( + base_permission="modify_us", + comment_permission="comment_us") + ), + + migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format( + base_permission="modify_us", + comment_permission="comment_us") + ), + + # tasks + migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format( + base_permission="modify_task", + comment_permission="comment_task") + ), + + migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format( + base_permission="modify_task", + comment_permission="comment_task") + ), + + # issues + migrations.RunSQL(UPDATE_PROJECTS_ANON_PERMISSIONS_SQL.format( + base_permission="modify_issue", + comment_permission="comment_issue") + ), + + migrations.RunSQL(UPDATE_PROJECTS_PUBLIC_PERMISSIONS_SQL.format( + base_permission="modify_issue", + comment_permission="comment_issue") + ) + ] diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 566fc79c..8dfafc41 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -18,14 +18,16 @@ from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin +from taiga.permissions.permissions import CommentAndOrUpdatePerm + class TaskPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_tasks') create_perms = HasProjectPerm('add_task') - update_perms = HasProjectPerm('modify_task') - partial_update_perms = HasProjectPerm('modify_task') + update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task') + partial_update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task') destroy_perms = HasProjectPerm('delete_task') list_perms = AllowAny() csv_perms = AllowAny() diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index 11aa5b73..c91ef2a7 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -18,14 +18,16 @@ from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin +from taiga.permissions.permissions import CommentAndOrUpdatePerm + class UserStoryPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_us') create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us') - update_perms = HasProjectPerm('modify_us') - partial_update_perms = HasProjectPerm('modify_us') + update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us') + partial_update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us') destroy_perms = HasProjectPerm('delete_us') list_perms = AllowAny() filters_data_perms = AllowAny() diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py index d85d56ea..e60458c7 100644 --- a/taiga/projects/wiki/permissions.py +++ b/taiga/projects/wiki/permissions.py @@ -19,6 +19,8 @@ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, IsAuthenticated, IsProjectAdmin, AllowAny, IsSuperUser) +from taiga.permissions.permissions import CommentAndOrUpdatePerm + class WikiPagePermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() @@ -26,8 +28,8 @@ class WikiPagePermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_wiki_pages') by_slug_perms = HasProjectPerm('view_wiki_pages') create_perms = HasProjectPerm('add_wiki_page') - update_perms = HasProjectPerm('modify_wiki_page') - partial_update_perms = HasProjectPerm('modify_wiki_page') + update_perms = CommentAndOrUpdatePerm('modify_wiki_page', 'comment_wiki_page') + partial_update_perms = CommentAndOrUpdatePerm('modify_wiki_page', 'comment_wiki_page') destroy_perms = HasProjectPerm('delete_wiki_page') list_perms = AllowAny() render_perms = AllowAny() diff --git a/taiga/users/migrations/0019_auto_20160519_1058.py b/taiga/users/migrations/0019_auto_20160519_1058.py new file mode 100644 index 00000000..69780084 --- /dev/null +++ b/taiga/users/migrations/0019_auto_20160519_1058.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-19 10:58 +from __future__ import unicode_literals + +from django.db import migrations +import djorm_pgarray.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0018_remove_vote_issues_in_roles_permissions_field'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='permissions', + field=djorm_pgarray.fields.TextArrayField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')], dbtype='text', default=[], verbose_name='permissions'), + ), + ] diff --git a/taiga/users/migrations/0020_auto_20160525_1229.py b/taiga/users/migrations/0020_auto_20160525_1229.py new file mode 100644 index 00000000..765eb3e1 --- /dev/null +++ b/taiga/users/migrations/0020_auto_20160525_1229.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-25 12:29 +from __future__ import unicode_literals + +from django.db import migrations + + +UPDATE_ROLES_PERMISSIONS_SQL = """ + UPDATE users_role + SET + PERMISSIONS = array_append(PERMISSIONS, '{comment_permission}') + WHERE + '{base_permission}' = ANY(PERMISSIONS) + AND + NOT '{comment_permission}' = ANY(PERMISSIONS) +""" + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0019_auto_20160519_1058'), + ] + + operations = [ + # user stories + migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format( + base_permission="modify_us", + comment_permission="comment_us") + ), + + # tasks + migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format( + base_permission="modify_task", + comment_permission="comment_task") + ), + + # issues + migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format( + base_permission="modify_issue", + comment_permission="comment_issue") + ), + + # issues + migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format( + base_permission="modify_issue", + comment_permission="comment_issue") + ) + ] diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index bb532473..536b4422 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -132,6 +132,55 @@ def data(): return m +def test_issue_list(client, data): + url = reverse('issues-list') + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + issues_data = json.loads(response.content.decode('utf-8')) + assert len(issues_data) == 4 + assert response.status_code == 200 + + +def test_issue_list_filter_by_project_ok(client, data): + url = "{}?project={}".format(reverse("issues-list"), data.public_project.pk) + + client.login(data.project_owner) + response = client.get(url) + + assert response.status_code == 200 + assert len(response.data) == 1 + + +def test_issue_list_filter_by_project_error(client, data): + url = "{}?project={}".format(reverse("issues-list"), "-ERROR-") + + client.login(data.project_owner) + response = client.get(url) + + assert response.status_code == 400 + + def test_issue_retrieve(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) @@ -156,7 +205,67 @@ def test_issue_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_issue_update(client, data): +def test_issue_create(client, data): + url = reverse('issues-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "severity": data.public_project.severities.all()[0].pk, + "priority": data.public_project.priorities.all()[0].pk, + "status": data.public_project.issue_statuses.all()[0].pk, + "type": data.public_project.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "severity": data.private_project1.severities.all()[0].pk, + "priority": data.private_project1.priorities.all()[0].pk, + "status": data.private_project1.issue_statuses.all()[0].pk, + "type": data.private_project1.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "severity": data.private_project2.severities.all()[0].pk, + "priority": data.private_project2.priorities.all()[0].pk, + "status": data.private_project2.issue_statuses.all()[0].pk, + "type": data.private_project2.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "severity": data.blocked_project.severities.all()[0].pk, + "priority": data.blocked_project.priorities.all()[0].pk, + "status": data.blocked_project.issue_statuses.all()[0].pk, + "type": data.blocked_project.issue_types.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_put_update(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) @@ -171,32 +280,116 @@ def test_issue_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - issue_data = IssueSerializer(data.public_issue).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', public_url, issue_data, users) - assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.public_issue).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] - issue_data = IssueSerializer(data.private_issue1).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', private_url1, issue_data, users) - assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.private_issue1).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] - issue_data = IssueSerializer(data.private_issue2).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', private_url2, issue_data, users) - assert results == [401, 403, 403, 200, 200] + issue_data = IssueSerializer(data.private_issue2).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] - issue_data = IssueSerializer(data.blocked_issue).data - issue_data["subject"] = "test" - issue_data = json.dumps(issue_data) - results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) - assert results == [401, 403, 403, 451, 451] + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["subject"] = "test" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] -def test_issue_update_with_project_change(client): +def test_issue_put_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + issue_data = IssueSerializer(data.public_issue).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue1).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue2).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_put_update_and_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + issue_data = IssueSerializer(data.public_issue).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', public_url, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue1).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url1, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.private_issue2).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', private_url2, issue_data, users) + assert results == [401, 403, 403, 200, 200] + + issue_data = IssueSerializer(data.blocked_issue).data + issue_data["subject"] = "test" + issue_data["comment"] = "test comment" + issue_data = json.dumps(issue_data) + results = helper_test_http_method(client, 'put', blocked_url, issue_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_put_update_with_project_change(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() user3 = f.UserFactory.create() @@ -309,139 +502,7 @@ def test_issue_update_with_project_change(client): issue.save() -def test_issue_delete(client, data): - public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) - private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) - private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) - blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - ] - - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url1, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url2, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', blocked_url, None, users) - assert results == [401, 403, 403, 451] - - -def test_issue_list(client, data): - url = reverse('issues-list') - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - issues_data = json.loads(response.content.decode('utf-8')) - assert len(issues_data) == 4 - assert response.status_code == 200 - - -def test_issue_list_filter_by_project_ok(client, data): - url = "{}?project={}".format(reverse("issues-list"), data.public_project.pk) - - client.login(data.project_owner) - response = client.get(url) - - assert response.status_code == 200 - assert len(response.data) == 1 - - -def test_issue_list_filter_by_project_error(client, data): - url = "{}?project={}".format(reverse("issues-list"), "-ERROR-") - - client.login(data.project_owner) - response = client.get(url) - - assert response.status_code == 400 - - -def test_issue_create(client, data): - url = reverse('issues-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({ - "subject": "test", - "ref": 1, - "project": data.public_project.pk, - "severity": data.public_project.severities.all()[0].pk, - "priority": data.public_project.priorities.all()[0].pk, - "status": data.public_project.issue_statuses.all()[0].pk, - "type": data.public_project.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 2, - "project": data.private_project1.pk, - "severity": data.private_project1.severities.all()[0].pk, - "priority": data.private_project1.priorities.all()[0].pk, - "status": data.private_project1.issue_statuses.all()[0].pk, - "type": data.private_project1.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.private_project2.pk, - "severity": data.private_project2.severities.all()[0].pk, - "priority": data.private_project2.priorities.all()[0].pk, - "status": data.private_project2.issue_statuses.all()[0].pk, - "type": data.private_project2.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.blocked_project.pk, - "severity": data.blocked_project.severities.all()[0].pk, - "priority": data.blocked_project.priorities.all()[0].pk, - "status": data.blocked_project.issue_statuses.all()[0].pk, - "type": data.blocked_project.issue_types.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_issue_patch(client, data): +def test_issue_patch_update(client, data): public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) @@ -473,7 +534,110 @@ def test_issue_patch(client, data): assert results == [401, 403, 403, 451, 451] -def test_issue_bulk_create(client, data): +def test_issue_patch_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_issue.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_issue1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_issue2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_issue.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_patch_update_and_comment(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_issue.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_issue1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_issue2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_issue.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_issue_delete(client, data): + public_url = reverse('issues-detail', kwargs={"pk": data.public_issue.pk}) + private_url1 = reverse('issues-detail', kwargs={"pk": data.private_issue1.pk}) + private_url2 = reverse('issues-detail', kwargs={"pk": data.private_issue2.pk}) + blocked_url = reverse('issues-detail', kwargs={"pk": data.blocked_issue.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + +def test_issue_action_bulk_create(client, data): data.public_issue.project.default_issue_status = f.IssueStatusFactory() data.public_issue.project.default_issue_type = f.IssueTypeFactory() data.public_issue.project.default_priority = f.PriorityFactory() @@ -633,34 +797,6 @@ def test_issue_voters_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_issues_csv(client, data): - url = reverse('issues-csv') - csv_public_uuid = data.public_project.issues_csv_uuid - csv_private1_uuid = data.private_project1.issues_csv_uuid - csv_private2_uuid = data.private_project2.issues_csv_uuid - csv_blocked_uuid = data.blocked_project.issues_csv_uuid - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - def test_issue_action_watch(client, data): public_url = reverse('issues-watch', kwargs={"pk": data.public_issue.pk}) private_url1 = reverse('issues-watch', kwargs={"pk": data.private_issue1.pk}) @@ -762,3 +898,31 @@ def test_issue_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_issues_csv(client, data): + url = reverse('issues-csv') + csv_public_uuid = data.public_project.issues_csv_uuid + csv_private1_uuid = data.private_project1.issues_csv_uuid + csv_private2_uuid = data.private_project2.issues_csv_uuid + csv_blocked_uuid = data.blocked_project.issues_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index aff81389..63a1d63e 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -142,6 +142,36 @@ def data(): return m +def test_task_list(client, data): + url = reverse('tasks-list') + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + tasks_data = json.loads(response.content.decode('utf-8')) + assert len(tasks_data) == 4 + assert response.status_code == 200 + + def test_task_retrieve(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) @@ -166,7 +196,55 @@ def test_task_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_task_update(client, data): +def test_task_create(client, data): + url = reverse('tasks-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "status": data.public_project.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "status": data.private_project1.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "status": data.private_project2.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "status": data.blocked_project.task_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_put_update(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) @@ -181,32 +259,116 @@ def test_task_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - task_data = TaskSerializer(data.public_task).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', public_url, task_data, users) - assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.public_task).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] - task_data = TaskSerializer(data.private_task1).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', private_url1, task_data, users) - assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.private_task1).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] - task_data = TaskSerializer(data.private_task2).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', private_url2, task_data, users) - assert results == [401, 403, 403, 200, 200] + task_data = TaskSerializer(data.private_task2).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] - task_data = TaskSerializer(data.blocked_task).data - task_data["subject"] = "test" - task_data = json.dumps(task_data) - results = helper_test_http_method(client, 'put', blocked_url, task_data, users) - assert results == [401, 403, 403, 451, 451] + task_data = TaskSerializer(data.blocked_task).data + task_data["subject"] = "test" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] -def test_task_update_with_project_change(client): +def test_task_put_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + task_data = TaskSerializer(data.public_task).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task1).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task2).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.blocked_task).data + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_put_update_and_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + task_data = TaskSerializer(data.public_task).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', public_url, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task1).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url1, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.private_task2).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', private_url2, task_data, users) + assert results == [401, 403, 403, 200, 200] + + task_data = TaskSerializer(data.blocked_task).data + task_data["subject"] = "test" + task_data["comment"] = "test comment" + task_data = json.dumps(task_data) + results = helper_test_http_method(client, 'put', blocked_url, task_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_put_update_with_project_change(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() user3 = f.UserFactory.create() @@ -301,107 +463,7 @@ def test_task_update_with_project_change(client): task.save() -def test_task_delete(client, data): - public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) - private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) - private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) - blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - ] - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url1, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url2, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', blocked_url, None, users) - assert results == [401, 403, 403, 451] - - -def test_task_list(client, data): - url = reverse('tasks-list') - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - tasks_data = json.loads(response.content.decode('utf-8')) - assert len(tasks_data) == 4 - assert response.status_code == 200 - - -def test_task_create(client, data): - url = reverse('tasks-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({ - "subject": "test", - "ref": 1, - "project": data.public_project.pk, - "status": data.public_project.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 2, - "project": data.private_project1.pk, - "status": data.private_project1.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.private_project2.pk, - "status": data.private_project2.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "subject": "test", - "ref": 3, - "project": data.blocked_project.pk, - "status": data.blocked_project.task_statuses.all()[0].pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_task_patch(client, data): +def test_task_patch_update(client, data): public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) @@ -433,6 +495,108 @@ def test_task_patch(client, data): assert results == [401, 403, 403, 451, 451] +def test_task_patch_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_task.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_task1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_task2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_task.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_patch_update_and_comment(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_task.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_task1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_task2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_task.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_task_delete(client, data): + public_url = reverse('tasks-detail', kwargs={"pk": data.public_task.pk}) + private_url1 = reverse('tasks-detail', kwargs={"pk": data.private_task1.pk}) + private_url2 = reverse('tasks-detail', kwargs={"pk": data.private_task2.pk}) + blocked_url = reverse('tasks-detail', kwargs={"pk": data.blocked_task.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + def test_task_action_bulk_create(client, data): url = reverse('tasks-bulk-create') @@ -586,34 +750,6 @@ def test_task_voters_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_tasks_csv(client, data): - url = reverse('tasks-csv') - csv_public_uuid = data.public_project.tasks_csv_uuid - csv_private1_uuid = data.private_project1.tasks_csv_uuid - csv_private2_uuid = data.private_project1.tasks_csv_uuid - csv_blocked_uuid = data.blocked_project.tasks_csv_uuid - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - def test_task_action_watch(client, data): public_url = reverse('tasks-watch', kwargs={"pk": data.public_task.pk}) private_url1 = reverse('tasks-watch', kwargs={"pk": data.private_task1.pk}) @@ -716,3 +852,31 @@ def test_task_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_tasks_csv(client, data): + url = reverse('tasks-csv') + csv_public_uuid = data.public_project.tasks_csv_uuid + csv_private1_uuid = data.private_project1.tasks_csv_uuid + csv_private2_uuid = data.private_project1.tasks_csv_uuid + csv_blocked_uuid = data.blocked_project.tasks_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 4da7b081..cb5f78ff 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -138,6 +138,36 @@ def data(): return m +def test_user_story_list(client, data): + url = reverse('userstories-list') + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + userstories_data = json.loads(response.content.decode('utf-8')) + assert len(userstories_data) == 4 + assert response.status_code == 200 + + def test_user_story_retrieve(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) @@ -162,7 +192,35 @@ def test_user_story_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_user_story_update(client, data): +def test_user_story_create(client, data): + url = reverse('userstories-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_put_update(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) @@ -201,7 +259,92 @@ def test_user_story_update(client, data): results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) assert results == [401, 403, 403, 451, 451] -def test_user_story_update_with_project_change(client): + +def test_user_story_put_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_put_update_and_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + user_story_data = UserStorySerializer(data.public_user_story).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', public_url, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story1).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url1, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.private_user_story2).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', private_url2, user_story_data, users) + assert results == [401, 403, 403, 200, 200] + + user_story_data = UserStorySerializer(data.blocked_user_story).data + user_story_data["subject"] = "test" + user_story_data["comment"] = "test comment" + user_story_data = json.dumps(user_story_data) + results = helper_test_http_method(client, 'put', blocked_url, user_story_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_put_update_with_project_change(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() user3 = f.UserFactory.create() @@ -296,87 +439,7 @@ def test_user_story_update_with_project_change(client): us.save() -def test_user_story_delete(client, data): - public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) - private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) - private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) - blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - ] - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url1, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url2, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', blocked_url, None, users) - assert results == [401, 403, 403, 451] - - -def test_user_story_list(client, data): - url = reverse('userstories-list') - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - userstories_data = json.loads(response.content.decode('utf-8')) - assert len(userstories_data) == 4 - assert response.status_code == 200 - - -def test_user_story_create(client, data): - url = reverse('userstories-list') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - create_data = json.dumps({"subject": "test", "ref": 1, "project": data.public_project.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({"subject": "test", "ref": 2, "project": data.private_project1.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({"subject": "test", "ref": 3, "project": data.private_project2.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({"subject": "test", "ref": 4, "project": data.blocked_project.pk}) - results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_user_story_patch(client, data): +def test_user_story_patch_update(client, data): public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) @@ -408,6 +471,109 @@ def test_user_story_patch(client, data): assert results == [401, 403, 403, 451, 451] +def test_user_story_patch_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_user_story.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_user_story1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_user_story2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_user_story.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_patch_update_and_comment(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_user_story.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_user_story1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_user_story2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_user_story.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_user_story_delete(client, data): + public_url = reverse('userstories-detail', kwargs={"pk": data.public_user_story.pk}) + private_url1 = reverse('userstories-detail', kwargs={"pk": data.private_user_story1.pk}) + private_url2 = reverse('userstories-detail', kwargs={"pk": data.private_user_story2.pk}) + blocked_url = reverse('userstories-detail', kwargs={"pk": data.blocked_user_story.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + + def test_user_story_action_bulk_create(client, data): url = reverse('userstories-bulk-create') @@ -580,30 +746,6 @@ def test_user_story_voters_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -def test_user_stories_csv(client, data): - url = reverse('userstories-csv') - csv_public_uuid = data.public_project.userstories_csv_uuid - csv_private1_uuid = data.private_project1.userstories_csv_uuid - csv_private2_uuid = data.private_project1.userstories_csv_uuid - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) - assert results == [200, 200, 200, 200, 200] - - def test_user_story_action_watch(client, data): public_url = reverse('userstories-watch', kwargs={"pk": data.public_user_story.pk}) private_url1 = reverse('userstories-watch', kwargs={"pk": data.private_user_story1.pk}) @@ -706,3 +848,27 @@ def test_userstory_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_user_stories_action_csv(client, data): + url = reverse('userstories-csv') + csv_public_uuid = data.public_project.userstories_csv_uuid + csv_private1_uuid = data.private_project1.userstories_csv_uuid + csv_private2_uuid = data.private_project1.userstories_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index aaee4e53..e981aa75 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -111,91 +111,9 @@ def data(): return m -def test_wiki_page_retrieve(client, data): - public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) - private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) - private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) - blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url1, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url2, None, users) - assert results == [401, 403, 403, 200, 200] - results = helper_test_http_method(client, 'get', blocked_url, None, users) - assert results == [401, 403, 403, 200, 200] - - -def test_wiki_page_update(client, data): - public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) - private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) - private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) - blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - wiki_page_data = WikiPageSerializer(data.public_wiki_page).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) - assert results == [401, 403, 403, 200, 200] - - wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) - assert results == [401, 403, 403, 200, 200] - - wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) - assert results == [401, 403, 403, 200, 200] - - wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data - wiki_page_data["content"] = "test" - wiki_page_data = json.dumps(wiki_page_data) - results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) - assert results == [401, 403, 403, 451, 451] - - -def test_wiki_page_delete(client, data): - public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) - private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) - private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) - blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - ] - results = helper_test_http_method(client, 'delete', public_url, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url1, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', private_url2, None, users) - assert results == [401, 403, 403, 204] - results = helper_test_http_method(client, 'delete', blocked_url, None, users) - assert results == [401, 403, 403, 451] - +############################################## +## WIKI PAGES +############################################## def test_wiki_page_list(client, data): url = reverse('wiki-list') @@ -227,6 +145,30 @@ def test_wiki_page_list(client, data): assert response.status_code == 200 +def test_wiki_page_retrieve(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + def test_wiki_page_create(client, data): url = reverse('wiki-list') @@ -270,7 +212,8 @@ def test_wiki_page_create(client, data): results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiPage.objects.all().delete()) assert results == [401, 403, 403, 451, 451] -def test_wiki_page_patch(client, data): + +def test_wiki_page_put_update(client, data): public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) @@ -285,68 +228,36 @@ def test_wiki_page_patch(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["content"] = "test" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] -def test_wiki_page_action_render(client, data): - url = reverse('wiki-render') - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - post_data = json.dumps({"content": "test", "project_id": data.public_project.pk}) - results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [200, 200, 200, 200, 200] - - -def test_wiki_link_retrieve(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url1, None, users) - assert results == [200, 200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private_url2, None, users) - assert results == [401, 403, 403, 200, 200] - results = helper_test_http_method(client, 'get', blocked_url, None, users) - assert results == [401, 403, 403, 200, 200] - - -def test_wiki_link_update(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) +def test_wiki_page_put_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -357,35 +268,278 @@ def test_wiki_link_update(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users) - assert results == [401, 403, 403, 200, 200] + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] - wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data - wiki_link_data["title"] = "test" - wiki_link_data = json.dumps(wiki_link_data) - results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users) - assert results == [401, 403, 403, 451, 451] + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] -def test_wiki_link_delete(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + +def test_wiki_page_put_update_and_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + wiki_page_data = WikiPageSerializer(data.public_wiki_page).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', public_url, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page1).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.private_wiki_page2).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_page_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_page_data = WikiPageSerializer(data.blocked_wiki_page).data + wiki_page_data["slug"] = "test" + wiki_page_data["comment"] = "test comment" + wiki_page_data = json.dumps(wiki_page_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_page_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_put_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + membership1 = f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership2 = f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership3 = f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + membership4 = f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + wiki_page = f.WikiPageFactory.create(project=project1) + + url = reverse('wiki-detail', kwargs={"pk": wiki_page.pk}) + + # Test user with permissions in both projects + client.login(user1) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 200 + + wiki_page.project = project1 + wiki_page.save() + + # Test user with permissions in only origin project + client.login(user2) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 403 + + wiki_page.project = project1 + wiki_page.save() + + # Test user with permissions in only destionation project + client.login(user3) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 403 + + wiki_page.project = project1 + wiki_page.save() + + # Test user without permissions in the projects + client.login(user4) + + wiki_page_data = WikiPageSerializer(wiki_page).data + wiki_page_data["project"] = project2.id + wiki_page_data = json.dumps(wiki_page_data) + + response = client.put(url, data=wiki_page_data, content_type="application/json") + + assert response.status_code == 403 + + wiki_page.project = project1 + wiki_page.save() + + +def test_wiki_page_patch_update(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"content": "test", "version": data.public_wiki_page.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"content": "test", "version": data.blocked_wiki_page.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_patch_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_wiki_page.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_wiki_page2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_wiki_page.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_patch_update_and_comment(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.public_wiki_page.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.private_wiki_page2.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.private_wiki_page2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "content": "test", + "comment": "test comment", + "version": data.blocked_wiki_page.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_page_delete(client, data): + public_url = reverse('wiki-detail', kwargs={"pk": data.public_wiki_page.pk}) + private_url1 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page1.pk}) + private_url2 = reverse('wiki-detail', kwargs={"pk": data.private_wiki_page2.pk}) + blocked_url = reverse('wiki-detail', kwargs={"pk": data.blocked_wiki_page.pk}) users = [ None, @@ -403,38 +557,8 @@ def test_wiki_link_delete(client, data): assert results == [401, 403, 403, 451] -def test_wiki_link_list(client, data): - url = reverse('wiki-links-list') - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 2 - assert response.status_code == 200 - - client.login(data.registered_user) - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 2 - assert response.status_code == 200 - - client.login(data.project_member_with_perms) - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 4 - assert response.status_code == 200 - - client.login(data.project_owner) - - response = client.get(url) - wiki_links_data = json.loads(response.content.decode('utf-8')) - assert len(wiki_links_data) == 4 - assert response.status_code == 200 - - -def test_wiki_link_create(client, data): - url = reverse('wiki-links-list') +def test_wiki_page_action_render(client, data): + url = reverse('wiki-render') users = [ None, @@ -444,69 +568,9 @@ def test_wiki_link_create(client, data): data.project_owner ] - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.public_project.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.private_project1.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.private_project2.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 403, 403, 201, 201] - - create_data = json.dumps({ - "title": "test", - "href": "test", - "project": data.blocked_project.pk, - }) - results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) - assert results == [401, 403, 403, 451, 451] - - -def test_wiki_link_patch(client, data): - public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) - private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) - private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) - blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [401, 403, 403, 200, 200] - - patch_data = json.dumps({"title": "test"}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [401, 403, 403, 451, 451] + post_data = json.dumps({"content": "test", "project_id": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [200, 200, 200, 200, 200] def test_wikipage_action_watch(client, data): @@ -610,3 +674,199 @@ def test_wikipage_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'get', blocked_url, None, users) assert results == [401, 403, 403, 200, 200] + + +############################################## +## WIKI LINKS +############################################## + +def test_wiki_link_list(client, data): + url = reverse('wiki-links-list') + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + wiki_links_data = json.loads(response.content.decode('utf-8')) + assert len(wiki_links_data) == 4 + assert response.status_code == 200 + + +def test_wiki_link_retrieve(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_wiki_link_create(client, data): + url = reverse('wiki-links-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.public_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.private_project1.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.private_project2.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "title": "test", + "href": "test", + "project": data.blocked_project.pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users, lambda: WikiLink.objects.all().delete()) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_link_update(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + wiki_link_data = WikiLinkSerializer(data.public_wiki_link).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', public_url, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.private_wiki_link1).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', private_url1, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.private_wiki_link2).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', private_url2, wiki_link_data, users) + assert results == [401, 403, 403, 200, 200] + + wiki_link_data = WikiLinkSerializer(data.blocked_wiki_link).data + wiki_link_data["title"] = "test" + wiki_link_data = json.dumps(wiki_link_data) + results = helper_test_http_method(client, 'put', blocked_url, wiki_link_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_link_patch(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"title": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_wiki_link_delete(client, data): + public_url = reverse('wiki-links-detail', kwargs={"pk": data.public_wiki_link.pk}) + private_url1 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link1.pk}) + private_url2 = reverse('wiki-links-detail', kwargs={"pk": data.private_wiki_link2.pk}) + blocked_url = reverse('wiki-links-detail', kwargs={"pk": data.blocked_wiki_link.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] From b2724723963fede753a8a1abfbf464fa15e0a2f8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 24 May 2016 09:10:22 +0200 Subject: [PATCH 024/261] Adding endpoint to bulk updating the milestone for user stories --- taiga/projects/userstories/api.py | 122 +++++++++++++--------- taiga/projects/userstories/permissions.py | 1 + taiga/projects/userstories/serializers.py | 33 +++++- taiga/projects/userstories/services.py | 15 +++ tests/integration/test_userstories.py | 68 ++++++++++++ 5 files changed, 185 insertions(+), 54 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 745268cd..1b0b8035 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -17,7 +17,6 @@ from contextlib import suppress - from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ @@ -37,6 +36,7 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersVi from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.models import Project, UserStoryStatus +from taiga.projects.milestones.models import Milestone from taiga.projects.history.services import take_snapshot from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin @@ -86,32 +86,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return serializers.UserStorySerializer - def update(self, request, *args, **kwargs): - self.object = self.get_object_or_none() - project_id = request.DATA.get('project', None) - if project_id and self.object and self.object.project.id != project_id: - try: - new_project = Project.objects.get(pk=project_id) - self.check_permissions(request, "destroy", self.object) - self.check_permissions(request, "create", new_project) - - sprint_id = request.DATA.get('milestone', None) - if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: - request.DATA['milestone'] = None - - status_id = request.DATA.get('status', None) - if status_id is not None: - try: - old_status = self.object.project.us_statuses.get(pk=status_id) - new_status = new_project.us_statuses.get(slug=old_status.slug) - request.DATA['status'] = new_status.id - except UserStoryStatus.DoesNotExist: - request.DATA['status'] = new_project.default_us_status.id - except Project.DoesNotExist: - return response.BadRequest(_("The project doesn't exist")) - - return super().update(request, *args, **kwargs) - def get_queryset(self): qs = super().get_queryset() qs = qs.prefetch_related("role_points", @@ -126,6 +100,17 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi qs = self.attach_votes_attrs_to_queryset(qs) return self.attach_watchers_attrs_to_queryset(qs) + def pre_conditions_on_save(self, obj): + super().pre_conditions_on_save(obj) + + if obj.milestone and obj.milestone.project != obj.project: + raise exc.PermissionDenied(_("You don't have permissions to set this sprint " + "to this user story.")) + + if obj.status and obj.status.project != obj.project: + raise exc.PermissionDenied(_("You don't have permissions to set this status " + "to this user story.")) + def pre_save(self, obj): # This is very ugly hack, but having # restframework is the only way to do it. @@ -155,16 +140,49 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi super().post_save(obj, created) - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) + @transaction.atomic + def create(self, *args, **kwargs): + response = super().create(*args, **kwargs) - if obj.milestone and obj.milestone.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this sprint " - "to this user story.")) + # Added comment to the origin (issue) + if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue: + self.object.generated_from_issue.save() - if obj.status and obj.status.project != obj.project: - raise exc.PermissionDenied(_("You don't have permissions to set this status " - "to this user story.")) + comment = _("Generating the user story #{ref} - {subject}") + comment = comment.format(ref=self.object.ref, subject=self.object.subject) + history = take_snapshot(self.object.generated_from_issue, + comment=comment, + user=self.request.user) + + self.send_notifications(self.object.generated_from_issue, history) + + return response + + def update(self, request, *args, **kwargs): + self.object = self.get_object_or_none() + project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: + try: + new_project = Project.objects.get(pk=project_id) + self.check_permissions(request, "destroy", self.object) + self.check_permissions(request, "create", new_project) + + sprint_id = request.DATA.get('milestone', None) + if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: + request.DATA['milestone'] = None + + status_id = request.DATA.get('status', None) + if status_id is not None: + try: + old_status = self.object.project.us_statuses.get(pk=status_id) + new_status = new_project.us_statuses.get(slug=old_status.slug) + request.DATA['status'] = new_status.id + except UserStoryStatus.DoesNotExist: + request.DATA['status'] = new_project.default_us_status.id + except Project.DoesNotExist: + return response.BadRequest(_("The project doesn't exist")) + + return super().update(request, *args, **kwargs) @list_route(methods=["GET"]) def filters_data(self, request, *args, **kwargs): @@ -224,6 +242,23 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return response.Ok(user_stories_serialized.data) return response.BadRequest(serializer.errors) + @list_route(methods=["POST"]) + def bulk_update_milestone(self, request, **kwargs): + serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + project = get_object_or_404(Project, pk=data["project_id"]) + milestone = get_object_or_404(Milestone, pk=data["milestone_id"]) + + self.check_permissions(request, "bulk_update_milestone", project) + + services.update_userstories_milestone_in_bulk(data["bulk_stories"], milestone) + services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) + + return response.NoContent() + def _bulk_update_order(self, order_field, request, **kwargs): serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) if not serializer.is_valid(): @@ -255,23 +290,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def bulk_update_kanban_order(self, request, **kwargs): return self._bulk_update_order("kanban_order", request, **kwargs) - @transaction.atomic - def create(self, *args, **kwargs): - response = super().create(*args, **kwargs) - - # Added comment to the origin (issue) - if response.status_code == status.HTTP_201_CREATED and self.object.generated_from_issue: - self.object.generated_from_issue.save() - - comment = _("Generating the user story #{ref} - {subject}") - comment = comment.format(ref=self.object.ref, subject=self.object.subject) - history = take_snapshot(self.object.generated_from_issue, - comment=comment, - user=self.request.user) - - self.send_notifications(self.object.generated_from_issue, history) - - return response class UserStoryVotersViewSet(VotersViewSetMixin, ModelListViewSet): permission_classes = (permissions.UserStoryVotersPermission,) diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index c91ef2a7..51c6f01a 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -34,6 +34,7 @@ class UserStoryPermission(TaigaResourcePermission): csv_perms = AllowAny() bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_update_order_perms = HasProjectPerm('modify_us') + bulk_update_milestone_perms = HasProjectPerm('modify_us') upvote_perms = IsAuthenticated() & HasProjectPerm('view_us') downvote_perms = IsAuthenticated() & HasProjectPerm('view_us') watch_perms = IsAuthenticated() & HasProjectPerm('view_us') diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 0931cae8..0a856e8a 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -17,6 +17,7 @@ from django.apps import apps from taiga.base.api import serializers +from taiga.base.api.utils import get_object_or_404 from taiga.base.fields import TagsField from taiga.base.fields import PickledObjectField from taiga.base.fields import PgArrayField @@ -24,8 +25,9 @@ from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.utils import json from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator -from taiga.projects.validators import UserStoryStatusExistsValidator +from taiga.projects.models import Project +from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicUserStoryStatusSerializer @@ -142,3 +144,30 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serial class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer): project_id = serializers.IntegerField() bulk_stories = _UserStoryOrderBulkSerializer(many=True) + + +## Milestone bulk serializers + +class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer): + us_id = serializers.IntegerField() + + +class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer): + project_id = serializers.IntegerField() + milestone_id = serializers.IntegerField() + bulk_stories = _UserStoryMilestoneBulkSerializer(many=True) + + def validate(self, data): + """ + All the userstories and the milestone are from the same project + """ + user_story_ids = [us["us_id"] for us in data["bulk_stories"]] + project = get_object_or_404(Project, pk=data["project_id"]) + + if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): + raise serializers.ValidationError("all the user stories must be from the same project") + + if project.milestones.filter(id=data["milestone_id"]).count() != 1: + raise serializers.ValidationError("the milestone isn't valid for the project") + + return data diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index b0b881a6..c1884228 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -91,6 +91,21 @@ def update_userstories_order_in_bulk(bulk_data:list, field:str, project:object): db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory) +def update_userstories_milestone_in_bulk(bulk_data:list, milestone:object): + """ + Update the milestone of some user stories. + `bulk_data` should be a list of user story ids: + """ + user_story_ids = [us_data["us_id"] for us_data in bulk_data] + new_milestone_values = [{"milestone": milestone.id}] * len(user_story_ids) + + events.emit_event_for_ids(ids=user_story_ids, + content_type="userstories.userstory", + projectid=milestone.project.pk) + + db.update_in_bulk_with_ids(user_story_ids, new_milestone_values, model=models.UserStory) + + def snapshot_userstories_in_bulk(bulk_data, user): user_story_ids = [] for us_data in bulk_data: diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 8081eefd..bcb0a618 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -164,6 +164,74 @@ def test_api_update_orders_in_bulk(client): assert response3.status_code == 204, response3.data +def test_api_update_milestone_in_bulk(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + milestone = f.MilestoneFactory.create(project=project) + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": milestone.id, + "bulk_stories": [{"us_id": us1.id}, + {"us_id": us2.id}] + } + + client.login(project.owner) + + assert project.milestones.get(id=milestone.id).user_stories.count() == 0 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 204, response.data + assert project.milestones.get(id=milestone.id).user_stories.count() == 2 + + +def test_api_update_milestone_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + m1 = f.MilestoneFactory.create(project=project) + m2 = f.MilestoneFactory.create() + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": m2.id, + "bulk_stories": [{"us_id": us1.id}, + {"us_id": us2.id}] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert response.data["non_field_errors"][0] == "the milestone isn't valid for the project" + + +def test_api_update_milestone_in_bulk_invalid_userstories(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory() + milestone = f.MilestoneFactory.create(project=project) + + url = reverse("userstories-bulk-update-milestone") + data = { + "project_id": project.id, + "milestone_id": milestone.id, + "bulk_stories": [{"us_id": us1.id}, + {"us_id": us2.id}] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + assert response.data["non_field_errors"][0] == "all the user stories must be from the same project" + + def test_update_userstory_points(client): user1 = f.UserFactory.create() user2 = f.UserFactory.create() From 1556d45ae6d6ffd0f3d47b8ede3970a5233e7aa1 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 26 May 2016 14:43:33 +0200 Subject: [PATCH 025/261] Generating application token only when it doesn't exist --- taiga/external_apps/models.py | 3 ++- tests/integration/test_application_tokens.py | 25 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/taiga/external_apps/models.py b/taiga/external_apps/models.py index fcc4695c..979978f4 100644 --- a/taiga/external_apps/models.py +++ b/taiga/external_apps/models.py @@ -83,4 +83,5 @@ class ApplicationToken(models.Model): def generate_token(self): self.auth_code = None - self.token = _generate_uuid() + if not self.token: + self.token = _generate_uuid() diff --git a/tests/integration/test_application_tokens.py b/tests/integration/test_application_tokens.py index da6d986e..da381179 100644 --- a/tests/integration/test_application_tokens.py +++ b/tests/integration/test_application_tokens.py @@ -100,3 +100,28 @@ def test_token_validate(client): decyphered_token = encryption.decrypt(response.data["cyphered_token"], token.application.key)[0] decyphered_token = json.loads(decyphered_token.decode("utf-8")) assert decyphered_token["token"] == token.token + + +def test_token_validate_validated(client): + # Validating a validated token should update the token attribute + user = f.UserFactory.create() + application = f.ApplicationFactory(next_url="http://next.url") + token = f.ApplicationTokenFactory( + auth_code="test-auth-code", + state="test-state", + application=application, + token="existing-token") + + url = reverse("application-tokens-validate") + client.login(user) + + data = { + "application": token.application.id, + "auth_code": "test-auth-code", + "state": "test-state" + } + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + + token = models.ApplicationToken.objects.get(id=token.id) + assert token.token == "existing-token" From 532d633467eff04bc61cc7bc9ab58fa2be2844fb Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 30 May 2016 12:18:29 +0200 Subject: [PATCH 026/261] Improving-project-deletion --- taiga/projects/api.py | 11 ++++-- .../migrations/0043_auto_20160530_1004.py | 22 ++++++++++++ taiga/projects/models.py | 2 +- taiga/projects/services/__init__.py | 2 ++ taiga/projects/services/projects.py | 21 ++++++++++- tests/integration/test_projects.py | 35 +++++++++++++++++++ 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 taiga/projects/migrations/0043_auto_20160530_1004.py diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 17684fd3..b37250a5 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -20,6 +20,7 @@ from easy_thumbnails.source_generators import pil_image from dateutil.relativedelta import relativedelta from django.apps import apps +from django.conf import settings from django.db.models import signals, Prefetch from django.db.models import Value as V from django.db.models.functions import Coalesce @@ -442,9 +443,13 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.pre_delete(obj) self.pre_conditions_on_delete(obj) - obj.delete_related_content() - obj.delete() - self.post_delete(obj) + + services.orphan_project(obj) + if settings.CELERY_ENABLED: + services.delete_project.delay(obj.id) + else: + services.delete_project(obj.id) + return response.NoContent() diff --git a/taiga/projects/migrations/0043_auto_20160530_1004.py b/taiga/projects/migrations/0043_auto_20160530_1004.py new file mode 100644 index 00000000..5ad8f1ad --- /dev/null +++ b/taiga/projects/migrations/0043_auto_20160530_1004.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-30 10:04 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0042_auto_20160525_0911'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_projects', to=settings.AUTH_USER_MODEL, verbose_name='owner'), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 506c2736..4ff459e0 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -157,7 +157,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): default=timezone.now) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) - owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False, + owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name="owned_projects", verbose_name=_("owner")) members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects", through="Membership", verbose_name=_("members"), diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index 23d6334c..d33d51e9 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -47,6 +47,8 @@ from .projects import check_if_project_privacity_can_be_changed from .projects import check_if_project_can_be_created_or_updated from .projects import check_if_project_can_be_transfered from .projects import check_if_project_is_out_of_owner_limits +from .projects import orphan_project +from .projects import delete_project from .stats import get_stats_for_project_issues from .stats import get_stats_for_project diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py index 1ac6d2b7..43309851 100644 --- a/taiga/projects/services/projects.py +++ b/taiga/projects/services/projects.py @@ -15,8 +15,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps from django.utils.translation import ugettext as _ - +from taiga.celery import app ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships' ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships' @@ -151,3 +152,21 @@ def check_if_project_is_out_of_owner_limits(project): return True return False + + +def orphan_project(project): + project.memberships.filter(user=project.owner).delete() + project.owner = None + project.save() + + +@app.task +def delete_project(project_id): + Project = apps.get_model("projects", "Project") + try: + project = Project.objects.get(id=project_id) + except Project.DoesNotExist: + return + + project.delete_related_content() + project.delete() diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 28a0dbe3..fbc09722 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -19,6 +19,8 @@ from easy_thumbnails.files import generate_all_aliases, get_thumbnailer import os.path import pytest +from unittest import mock + pytestmark = pytest.mark.django_db class ExpiredSigner(signing.TimestampSigner): @@ -1814,3 +1816,36 @@ def test_public_project_when_project_has_unlimited_members(client): project.owner.max_memberships_public_projects = None assert check_if_project_is_out_of_owner_limits(project) == False + + +def test_delete_project_with_celery_enabled(client, settings): + settings.CELERY_ENABLED = True + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-detail", args=(project.id,)) + client.login(user) + + #delete_project task should have been launched + with mock.patch('taiga.projects.services.delete_project') as delete_project_mock: + response = client.json.delete(url) + assert response.status_code == 204 + project = Project.objects.get(id=project.id) + assert project.owner == None + assert project.memberships.count() == 0 + delete_project_mock.delay.assert_called_once_with(project.id) + + +def test_delete_project_with_celery_disabled(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-detail", args=(project.id,)) + client.login(user) + response = client.json.delete(url) + assert response.status_code == 204 + assert Project.objects.filter(id=project.id).count() == 0 From 3fd2d1cade716f264b2febc3627b1443a1d3e604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 30 May 2016 18:31:56 +0200 Subject: [PATCH 027/261] Fix a problem with a migration between master and stable branch --- taiga/projects/migrations/0043_auto_20160530_1004.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/migrations/0043_auto_20160530_1004.py b/taiga/projects/migrations/0043_auto_20160530_1004.py index 5ad8f1ad..101b3b6e 100644 --- a/taiga/projects/migrations/0043_auto_20160530_1004.py +++ b/taiga/projects/migrations/0043_auto_20160530_1004.py @@ -10,7 +10,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('projects', '0042_auto_20160525_0911'), + ('projects', '0040_remove_memberships_of_cancelled_users_acounts'), ] operations = [ From 3c15b0ab1a7b3b8dd3df124bd687c024e8ee28a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 30 May 2016 18:39:10 +0200 Subject: [PATCH 028/261] Create a merge migration to fix the problem between master and stable branches --- taiga/projects/migrations/0044_merge.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 taiga/projects/migrations/0044_merge.py diff --git a/taiga/projects/migrations/0044_merge.py b/taiga/projects/migrations/0044_merge.py new file mode 100644 index 00000000..6bf0227c --- /dev/null +++ b/taiga/projects/migrations/0044_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-30 16:36 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0043_auto_20160530_1004'), + ('projects', '0042_auto_20160525_0911'), + ] + + operations = [ + ] From 47907eedb4b044599ad080d96587c76852a61d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 30 May 2016 20:11:13 +0200 Subject: [PATCH 029/261] Apply PEP-263 to taiga project --- manage.py | 19 +++++++++++++++++++ settings/__init__.py | 1 + settings/celery.py | 1 + settings/common.py | 1 + settings/development.py | 1 + settings/local.py.example | 1 + settings/sr.py | 1 + settings/testing.py | 1 + settings/travis.py | 1 + taiga/__init__.py | 1 + taiga/auth/__init__.py | 1 - taiga/auth/api.py | 1 + taiga/auth/backends.py | 1 + taiga/auth/permissions.py | 1 + taiga/auth/serializers.py | 1 + taiga/auth/services.py | 1 + taiga/auth/signals.py | 1 + taiga/auth/tokens.py | 1 + taiga/base/__init__.py | 1 + taiga/base/api/__init__.py | 1 + taiga/base/api/authentication.py | 1 + taiga/base/api/fields.py | 1 + taiga/base/api/generics.py | 1 + taiga/base/api/mixins.py | 1 + taiga/base/api/negotiation.py | 1 + taiga/base/api/pagination.py | 1 + taiga/base/api/parsers.py | 1 + taiga/base/api/permissions.py | 1 + taiga/base/api/relations.py | 1 + taiga/base/api/renderers.py | 1 + taiga/base/api/request.py | 1 + taiga/base/api/reverse.py | 1 + taiga/base/api/serializers.py | 1 + taiga/base/api/settings.py | 1 + taiga/base/api/templatetags/api.py | 1 + taiga/base/api/throttling.py | 1 + taiga/base/api/urlpatterns.py | 1 + taiga/base/api/urls.py | 1 + taiga/base/api/utils.py | 1 + taiga/base/api/utils/__init__.py | 1 + taiga/base/api/utils/breadcrumbs.py | 1 + taiga/base/api/utils/encoders.py | 1 + taiga/base/api/utils/formatting.py | 1 + taiga/base/api/utils/mediatypes.py | 1 + taiga/base/api/views.py | 1 + taiga/base/api/viewsets.py | 1 + taiga/base/apps.py | 1 + taiga/base/connectors/exceptions.py | 1 + taiga/base/decorators.py | 1 + taiga/base/exceptions.py | 1 + taiga/base/fields.py | 1 + taiga/base/filters.py | 1 + taiga/base/formats/en/formats.py | 1 + taiga/base/formats/es/formats.py | 1 + taiga/base/mails.py | 1 + taiga/base/management/commands/test_emails.py | 1 + taiga/base/middleware/cors.py | 1 + taiga/base/neighbors.py | 1 + taiga/base/response.py | 1 + taiga/base/routers.py | 1 + taiga/base/signals/cleanup_files.py | 1 + taiga/base/signals/thumbnails.py | 1 + taiga/base/status.py | 1 + taiga/base/storage.py | 1 + taiga/base/tags.py | 1 + taiga/base/throttling.py | 1 + taiga/base/utils/contenttypes.py | 1 + taiga/base/utils/db.py | 1 + taiga/base/utils/dicts.py | 1 + taiga/base/utils/diff.py | 1 + taiga/base/utils/files.py | 1 + taiga/base/utils/functions.py | 1 + taiga/base/utils/iterators.py | 1 + taiga/base/utils/json.py | 1 + taiga/base/utils/sequence.py | 1 + taiga/base/utils/signals.py | 1 + taiga/base/utils/slug.py | 1 + taiga/base/utils/text.py | 1 + taiga/base/utils/thumbnails.py | 1 + taiga/base/utils/urls.py | 1 + taiga/celery.py | 1 + taiga/deferred.py | 1 + taiga/events/__init__.py | 1 + taiga/events/apps.py | 1 + taiga/events/backends/__init__.py | 1 + taiga/events/backends/base.py | 1 + taiga/events/backends/postgresql.py | 1 + taiga/events/backends/rabbitmq.py | 1 + taiga/events/events.py | 5 +++++ .../commands/emit_notification_message.py | 1 + taiga/events/middleware.py | 5 +++++ taiga/events/signal_handlers.py | 1 + taiga/export_import/api.py | 1 + taiga/export_import/exceptions.py | 1 + .../management/commands/dump_project.py | 1 + .../management/commands/load_dump.py | 1 + taiga/export_import/mixins.py | 1 + taiga/export_import/permissions.py | 1 + taiga/export_import/renderers.py | 1 + taiga/export_import/serializers.py | 1 + taiga/export_import/services/__init__.py | 1 + taiga/export_import/services/render.py | 1 + taiga/export_import/services/store.py | 1 + taiga/export_import/tasks.py | 1 + taiga/export_import/throttling.py | 1 + taiga/external_apps/admin.py | 1 + taiga/external_apps/api.py | 1 + taiga/external_apps/auth_backends.py | 1 + taiga/external_apps/encryption.py | 1 + taiga/external_apps/models.py | 1 + taiga/external_apps/permissions.py | 1 + taiga/external_apps/serializers.py | 1 + taiga/external_apps/services.py | 1 + taiga/feedback/__init__.py | 1 + taiga/feedback/admin.py | 1 + taiga/feedback/api.py | 1 + taiga/feedback/apps.py | 1 + taiga/feedback/models.py | 1 + taiga/feedback/permissions.py | 1 + taiga/feedback/routers.py | 1 + taiga/feedback/serializers.py | 1 + taiga/feedback/services.py | 1 + taiga/front/sitemaps/__init__.py | 5 +++-- taiga/front/sitemaps/base.py | 5 +++-- taiga/front/sitemaps/generics.py | 5 +++-- taiga/front/sitemaps/issues.py | 5 +++-- taiga/front/sitemaps/milestones.py | 5 +++-- taiga/front/sitemaps/projects.py | 5 +++-- taiga/front/sitemaps/tasks.py | 5 +++-- taiga/front/sitemaps/users.py | 5 +++-- taiga/front/sitemaps/userstories.py | 5 +++-- taiga/front/sitemaps/wiki.py | 5 +++-- taiga/front/templatetags/functions.py | 1 + taiga/front/urls.py | 1 + taiga/hooks/api.py | 1 + taiga/hooks/bitbucket/api.py | 1 + taiga/hooks/bitbucket/event_hooks.py | 1 + taiga/hooks/bitbucket/models.py | 1 + taiga/hooks/bitbucket/services.py | 1 + taiga/hooks/event_hooks.py | 1 + taiga/hooks/exceptions.py | 1 + taiga/hooks/github/api.py | 1 + taiga/hooks/github/event_hooks.py | 1 + taiga/hooks/github/models.py | 1 + taiga/hooks/github/services.py | 1 + taiga/hooks/gitlab/api.py | 1 + taiga/hooks/gitlab/event_hooks.py | 1 + taiga/hooks/gitlab/models.py | 1 + taiga/hooks/gitlab/services.py | 1 + taiga/locale/api.py | 1 + taiga/locale/permissions.py | 1 + taiga/mdrender/extensions/autolink.py | 1 + taiga/mdrender/extensions/automail.py | 1 + taiga/mdrender/extensions/semi_sane_lists.py | 1 + taiga/mdrender/extensions/spaced_link.py | 1 + taiga/mdrender/extensions/strikethrough.py | 1 + taiga/mdrender/extensions/target_link.py | 1 + taiga/mdrender/extensions/wikilinks.py | 1 + taiga/mdrender/service.py | 1 + taiga/mdrender/templatetags/__init__.py | 1 - taiga/mdrender/templatetags/functions.py | 1 + taiga/permissions/choices.py | 1 + taiga/permissions/permissions.py | 1 + taiga/permissions/services.py | 1 + taiga/projects/__init__.py | 1 + taiga/projects/admin.py | 1 + taiga/projects/api.py | 1 + taiga/projects/apps.py | 1 + taiga/projects/attachments/__init__.py | 1 + taiga/projects/attachments/admin.py | 1 + taiga/projects/attachments/api.py | 1 + taiga/projects/attachments/apps.py | 1 + .../management/commands/generate_sha1.py | 18 ++++++++++++++++++ taiga/projects/attachments/models.py | 1 + taiga/projects/attachments/permissions.py | 1 + taiga/projects/attachments/serializers.py | 1 + taiga/projects/attachments/services.py | 1 + taiga/projects/choices.py | 1 + taiga/projects/custom_attributes/admin.py | 1 + taiga/projects/custom_attributes/api.py | 1 + taiga/projects/custom_attributes/choices.py | 1 + taiga/projects/custom_attributes/models.py | 1 + .../projects/custom_attributes/permissions.py | 1 + .../projects/custom_attributes/serializers.py | 1 + taiga/projects/custom_attributes/services.py | 1 + taiga/projects/custom_attributes/signals.py | 1 + taiga/projects/filters.py | 1 + taiga/projects/history/api.py | 1 + taiga/projects/history/choices.py | 4 ++++ taiga/projects/history/freeze_impl.py | 1 + taiga/projects/history/mixins.py | 1 + taiga/projects/history/models.py | 4 ++++ taiga/projects/history/permissions.py | 1 + taiga/projects/history/serializers.py | 1 + taiga/projects/history/services.py | 4 ++++ .../projects/history/templatetags/__init__.py | 1 - .../history/templatetags/functions.py | 1 + taiga/projects/issues/__init__.py | 1 + taiga/projects/issues/admin.py | 1 + taiga/projects/issues/api.py | 1 + taiga/projects/issues/apps.py | 1 + taiga/projects/issues/models.py | 1 + taiga/projects/issues/permissions.py | 1 + taiga/projects/issues/serializers.py | 1 + taiga/projects/issues/services.py | 1 + taiga/projects/issues/signals.py | 1 + taiga/projects/likes/admin.py | 1 + taiga/projects/likes/mixins/serializers.py | 1 + taiga/projects/likes/mixins/viewsets.py | 1 + taiga/projects/likes/models.py | 1 + taiga/projects/likes/serializers.py | 1 + taiga/projects/likes/services.py | 1 + .../management/commands/sample_data.py | 1 + taiga/projects/milestones/admin.py | 1 + taiga/projects/milestones/api.py | 1 + taiga/projects/milestones/models.py | 1 + taiga/projects/milestones/permissions.py | 1 + taiga/projects/milestones/serializers.py | 1 + taiga/projects/milestones/services.py | 1 + taiga/projects/milestones/validators.py | 18 ++++++++++++++++++ taiga/projects/mixins/blocked.py | 1 + taiga/projects/mixins/on_destroy.py | 1 + taiga/projects/mixins/ordering.py | 1 + taiga/projects/mixins/serializers.py | 1 + taiga/projects/models.py | 1 + taiga/projects/notifications/admin.py | 1 + taiga/projects/notifications/api.py | 1 + taiga/projects/notifications/choices.py | 1 + .../management/commands/send_notifications.py | 1 + taiga/projects/notifications/mixins.py | 1 + taiga/projects/notifications/models.py | 1 + taiga/projects/notifications/permissions.py | 1 + taiga/projects/notifications/serializers.py | 1 + taiga/projects/notifications/services.py | 1 + taiga/projects/notifications/utils.py | 1 + taiga/projects/notifications/validators.py | 1 + taiga/projects/occ/__init__.py | 1 + taiga/projects/occ/mixins.py | 1 + taiga/projects/permissions.py | 1 + taiga/projects/references/api.py | 1 + taiga/projects/references/models.py | 1 + taiga/projects/references/permissions.py | 1 + taiga/projects/references/sequences.py | 1 + taiga/projects/references/serializers.py | 1 + taiga/projects/references/services.py | 1 + taiga/projects/serializers.py | 1 + taiga/projects/services/__init__.py | 1 + taiga/projects/services/bulk_update_order.py | 1 + taiga/projects/services/filters.py | 1 + taiga/projects/services/invitations.py | 18 ++++++++++++++++++ taiga/projects/services/logo.py | 1 + taiga/projects/services/members.py | 1 + taiga/projects/services/modules_config.py | 1 + taiga/projects/services/projects.py | 1 + taiga/projects/services/stats.py | 1 + taiga/projects/services/tags_colors.py | 1 + taiga/projects/services/transfer.py | 1 + taiga/projects/signals.py | 1 + taiga/projects/tasks/__init__.py | 1 + taiga/projects/tasks/admin.py | 1 + taiga/projects/tasks/api.py | 1 + taiga/projects/tasks/apps.py | 1 + taiga/projects/tasks/models.py | 1 + taiga/projects/tasks/permissions.py | 1 + taiga/projects/tasks/serializers.py | 1 + taiga/projects/tasks/services.py | 1 + taiga/projects/tasks/signals.py | 1 + taiga/projects/tasks/validators.py | 18 ++++++++++++++++++ taiga/projects/translations.py | 1 + taiga/projects/userstories/__init__.py | 1 + taiga/projects/userstories/admin.py | 1 + taiga/projects/userstories/api.py | 1 + taiga/projects/userstories/apps.py | 1 + taiga/projects/userstories/models.py | 1 + taiga/projects/userstories/permissions.py | 1 + taiga/projects/userstories/serializers.py | 1 + taiga/projects/userstories/services.py | 1 + taiga/projects/userstories/signals.py | 1 + taiga/projects/userstories/validators.py | 1 + taiga/projects/validators.py | 1 + taiga/projects/votes/admin.py | 1 + taiga/projects/votes/mixins/serializers.py | 1 + taiga/projects/votes/mixins/viewsets.py | 1 + taiga/projects/votes/models.py | 1 + taiga/projects/votes/serializers.py | 1 + taiga/projects/votes/services.py | 1 + taiga/projects/votes/utils.py | 1 + taiga/projects/wiki/admin.py | 1 + taiga/projects/wiki/api.py | 1 + taiga/projects/wiki/models.py | 1 + taiga/projects/wiki/permissions.py | 1 + taiga/projects/wiki/serializers.py | 1 + taiga/routers.py | 1 + taiga/searches/api.py | 1 + taiga/searches/serializers.py | 1 + taiga/searches/services.py | 1 + taiga/stats/__init__.py | 1 + taiga/stats/api.py | 1 + taiga/stats/apps.py | 1 + taiga/stats/permissions.py | 1 + taiga/stats/routers.py | 1 + taiga/stats/services.py | 1 + taiga/timeline/__init__.py | 1 + taiga/timeline/api.py | 1 + taiga/timeline/apps.py | 1 + ...lear_unnecessary_new_membership_entries.py | 1 + .../_rebuild_timeline_for_user_creation.py | 1 + .../_update_timeline_for_updated_tasks.py | 1 + .../management/commands/rebuild_timeline.py | 1 + ...rebuild_timeline_iterating_per_projects.py | 1 + taiga/timeline/models.py | 1 + taiga/timeline/permissions.py | 1 + taiga/timeline/serializers.py | 1 + taiga/timeline/service.py | 1 + taiga/timeline/signals.py | 1 + taiga/timeline/timeline_implementations.py | 1 + taiga/urls.py | 1 + taiga/users/admin.py | 1 + taiga/users/api.py | 1 + taiga/users/filters.py | 1 + taiga/users/forms.py | 1 + taiga/users/gravatar.py | 1 + taiga/users/models.py | 1 + taiga/users/permissions.py | 1 + taiga/users/serializers.py | 1 + taiga/users/services.py | 1 + taiga/users/signals.py | 1 + taiga/users/validators.py | 1 + taiga/userstorage/api.py | 1 + taiga/userstorage/filters.py | 1 + taiga/userstorage/models.py | 1 + taiga/userstorage/permissions.py | 1 + taiga/userstorage/serializers.py | 1 + taiga/webhooks/__init__.py | 1 + taiga/webhooks/api.py | 1 + taiga/webhooks/apps.py | 1 + taiga/webhooks/models.py | 1 + taiga/webhooks/permissions.py | 1 + taiga/webhooks/serializers.py | 1 + taiga/webhooks/signal_handlers.py | 1 + taiga/webhooks/tasks.py | 1 + taiga/wsgi.py | 19 +++++++++++++++++++ tests/conftest.py | 1 + tests/factories.py | 1 + tests/fixtures.py | 1 + .../test_application_tokens_resources.py | 1 + .../test_attachment_resources.py | 1 + .../test_auth_resources.py | 1 + .../resources_permissions/test_feedback.py | 1 + .../test_history_resources.py | 1 + .../test_issues_custom_attributes_resource.py | 1 + .../test_issues_resources.py | 1 + .../test_milestones_resources.py | 1 + .../test_modules_resources.py | 1 + .../test_projects_choices_resources.py | 1 + .../test_projects_resource.py | 1 + .../test_resolver_resources.py | 1 + .../test_search_resources.py | 1 + .../test_storage_resources.py | 1 + .../test_tasks_custom_attributes_resource.py | 1 + .../test_tasks_resources.py | 1 + .../test_timelines_resources.py | 1 + .../test_users_resources.py | 1 + ..._userstories_custom_attributes_resource.py | 1 + .../test_userstories_resources.py | 1 + .../test_webhooks_resources.py | 1 + .../test_wiki_resources.py | 1 + tests/integration/test_application_tokens.py | 1 + tests/integration/test_attachments.py | 1 + tests/integration/test_auth_api.py | 1 + .../test_custom_attributes_issues.py | 1 + .../test_custom_attributes_tasks.py | 1 + .../test_custom_attributes_user_stories.py | 1 + tests/integration/test_exporter_api.py | 1 + tests/integration/test_fan_projects.py | 1 + tests/integration/test_feedback.py | 1 + tests/integration/test_history.py | 1 + tests/integration/test_hooks_bitbucket.py | 1 + tests/integration/test_hooks_github.py | 1 + tests/integration/test_hooks_gitlab.py | 1 + tests/integration/test_importer_api.py | 1 + tests/integration/test_issues.py | 1 + tests/integration/test_mdrender.py | 1 + tests/integration/test_memberships.py | 1 + tests/integration/test_milestones.py | 1 + tests/integration/test_models.py | 1 + tests/integration/test_neighbors.py | 1 + tests/integration/test_notifications.py | 1 + tests/integration/test_occ.py | 1 + tests/integration/test_permissions.py | 1 + tests/integration/test_projects.py | 1 + .../integration/test_references_sequences.py | 1 + tests/integration/test_roles.py | 1 + tests/integration/test_searches.py | 1 + tests/integration/test_stats.py | 1 + tests/integration/test_tasks.py | 1 + tests/integration/test_throwttling.py | 1 + tests/integration/test_timeline.py | 1 + tests/integration/test_totals_projects.py | 1 + tests/integration/test_us_autoclosing.py | 1 + tests/integration/test_users.py | 1 + tests/integration/test_userstorage_api.py | 1 + tests/integration/test_userstories.py | 1 + tests/integration/test_vote_issues.py | 1 + tests/integration/test_vote_tasks.py | 1 + tests/integration/test_vote_userstories.py | 1 + tests/integration/test_votes.py | 1 + tests/integration/test_watch_issues.py | 1 + tests/integration/test_watch_milestones.py | 1 + tests/integration/test_watch_projects.py | 1 + tests/integration/test_watch_tasks.py | 1 + tests/integration/test_watch_userstories.py | 1 + tests/integration/test_watch_wikipages.py | 1 + tests/integration/test_webhooks_issues.py | 1 + tests/integration/test_webhooks_milestones.py | 1 + tests/integration/test_webhooks_signals.py | 1 + tests/integration/test_webhooks_tasks.py | 1 + .../integration/test_webhooks_userstories.py | 1 + tests/integration/test_webhooks_wikipages.py | 1 + tests/models.py | 1 + tests/unit/conftest.py | 1 + tests/unit/test_base_api_permissions.py | 1 + tests/unit/test_deferred.py | 1 + tests/unit/test_export.py | 1 + tests/unit/test_gravatar.py | 1 + tests/unit/test_mdrender.py | 1 + tests/unit/test_serializer_mixins.py | 1 + tests/unit/test_slug.py | 1 + tests/unit/test_timeline.py | 1 + tests/unit/test_tokens.py | 1 + tests/unit/test_utils.py | 1 + tests/utils.py | 1 + 432 files changed, 570 insertions(+), 23 deletions(-) diff --git a/manage.py b/manage.py index f9726f9e..63a6358b 100755 --- a/manage.py +++ b/manage.py @@ -1,4 +1,23 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + import os import sys diff --git a/settings/__init__.py b/settings/__init__.py index 23fd84b8..a5fbfb3e 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/celery.py b/settings/celery.py index 05444695..82e4d92f 100644 --- a/settings/celery.py +++ b/settings/celery.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/common.py b/settings/common.py index 568be60c..f227d97c 100644 --- a/settings/common.py +++ b/settings/common.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/development.py b/settings/development.py index 6a7f981d..8611719d 100644 --- a/settings/development.py +++ b/settings/development.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/local.py.example b/settings/local.py.example index 09e53ca4..4ae5a8ab 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/sr.py b/settings/sr.py index bc58e8e0..1477e682 100644 --- a/settings/sr.py +++ b/settings/sr.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/testing.py b/settings/testing.py index 6862a5b1..29dd67d2 100644 --- a/settings/testing.py +++ b/settings/testing.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/settings/travis.py b/settings/travis.py index 8481688c..13c0f9c2 100644 --- a/settings/travis.py +++ b/settings/travis.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/__init__.py b/taiga/__init__.py index ee793c4a..8be9bc25 100644 --- a/taiga/__init__.py +++ b/taiga/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/auth/__init__.py b/taiga/auth/__init__.py index 8b137891..e69de29b 100644 --- a/taiga/auth/__init__.py +++ b/taiga/auth/__init__.py @@ -1 +0,0 @@ - diff --git a/taiga/auth/api.py b/taiga/auth/api.py index 7f3d7ad8..5d14d18f 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/auth/backends.py b/taiga/auth/backends.py index 920d3f20..7813d457 100644 --- a/taiga/auth/backends.py +++ b/taiga/auth/backends.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/auth/permissions.py b/taiga/auth/permissions.py index 3e648abd..854eff26 100644 --- a/taiga/auth/permissions.py +++ b/taiga/auth/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso diff --git a/taiga/auth/serializers.py b/taiga/auth/serializers.py index 52a1ae17..8e8df4e2 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/auth/services.py b/taiga/auth/services.py index 5015a02e..a76c350f 100644 --- a/taiga/auth/services.py +++ b/taiga/auth/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/auth/signals.py b/taiga/auth/signals.py index ed5782e9..9e375836 100644 --- a/taiga/auth/signals.py +++ b/taiga/auth/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/auth/tokens.py b/taiga/auth/tokens.py index 58fe2938..a939b748 100644 --- a/taiga/auth/tokens.py +++ b/taiga/auth/tokens.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/__init__.py b/taiga/base/__init__.py index 1e06114a..5a7db2f0 100644 --- a/taiga/base/__init__.py +++ b/taiga/base/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/__init__.py b/taiga/base/api/__init__.py index b2c17b0d..ad481185 100644 --- a/taiga/base/api/__init__.py +++ b/taiga/base/api/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/authentication.py b/taiga/base/api/authentication.py index 40385395..d44c69d5 100644 --- a/taiga/base/api/authentication.py +++ b/taiga/base/api/authentication.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index 365e4070..7dfa2c0a 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index 82d3f487..158d712d 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index 1125fcd9..89af6984 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/negotiation.py b/taiga/base/api/negotiation.py index fdc16717..fd2a7028 100644 --- a/taiga/base/api/negotiation.py +++ b/taiga/base/api/negotiation.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/pagination.py b/taiga/base/api/pagination.py index 147a7bb7..46c1540d 100644 --- a/taiga/base/api/pagination.py +++ b/taiga/base/api/pagination.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/parsers.py b/taiga/base/api/parsers.py index 920a78cb..42d436f5 100644 --- a/taiga/base/api/parsers.py +++ b/taiga/base/api/parsers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/permissions.py b/taiga/base/api/permissions.py index 19b366fc..b03d6c18 100644 --- a/taiga/base/api/permissions.py +++ b/taiga/base/api/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/relations.py b/taiga/base/api/relations.py index f60c8ac5..60ba9a6e 100644 --- a/taiga/base/api/relations.py +++ b/taiga/base/api/relations.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/renderers.py b/taiga/base/api/renderers.py index 21c053e0..bc26e95b 100644 --- a/taiga/base/api/renderers.py +++ b/taiga/base/api/renderers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/request.py b/taiga/base/api/request.py index 3083b06d..059ece06 100644 --- a/taiga/base/api/request.py +++ b/taiga/base/api/request.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/reverse.py b/taiga/base/api/reverse.py index 2451fc1c..4d4de867 100644 --- a/taiga/base/api/reverse.py +++ b/taiga/base/api/reverse.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 55ae824f..a9e5f139 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py index 9b894be6..1a3d01ba 100644 --- a/taiga/base/api/settings.py +++ b/taiga/base/api/settings.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/templatetags/api.py b/taiga/base/api/templatetags/api.py index b2710a18..c40b0aa4 100644 --- a/taiga/base/api/templatetags/api.py +++ b/taiga/base/api/templatetags/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/throttling.py b/taiga/base/api/throttling.py index 3ff19ff7..b23bea09 100644 --- a/taiga/base/api/throttling.py +++ b/taiga/base/api/throttling.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/urlpatterns.py b/taiga/base/api/urlpatterns.py index 8bfd6f1c..33548a07 100644 --- a/taiga/base/api/urlpatterns.py +++ b/taiga/base/api/urlpatterns.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/urls.py b/taiga/base/api/urls.py index 30b2cf4a..01be0d71 100644 --- a/taiga/base/api/urls.py +++ b/taiga/base/api/urls.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/utils.py b/taiga/base/api/utils.py index 77148b49..30318d5e 100644 --- a/taiga/base/api/utils.py +++ b/taiga/base/api/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/utils/__init__.py b/taiga/base/api/utils/__init__.py index 227f56fd..b6198a45 100644 --- a/taiga/base/api/utils/__init__.py +++ b/taiga/base/api/utils/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/utils/breadcrumbs.py b/taiga/base/api/utils/breadcrumbs.py index 57965bce..0d10758f 100644 --- a/taiga/base/api/utils/breadcrumbs.py +++ b/taiga/base/api/utils/breadcrumbs.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/utils/encoders.py b/taiga/base/api/utils/encoders.py index 0f878914..be307d25 100644 --- a/taiga/base/api/utils/encoders.py +++ b/taiga/base/api/utils/encoders.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/utils/formatting.py b/taiga/base/api/utils/formatting.py index fbeb2534..f2decc0b 100644 --- a/taiga/base/api/utils/formatting.py +++ b/taiga/base/api/utils/formatting.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/utils/mediatypes.py b/taiga/base/api/utils/mediatypes.py index 02e09b5d..c0dc1266 100644 --- a/taiga/base/api/utils/mediatypes.py +++ b/taiga/base/api/utils/mediatypes.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/views.py b/taiga/base/api/views.py index 791523d4..8c6dfd09 100644 --- a/taiga/base/api/views.py +++ b/taiga/base/api/views.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py index af2d2789..95b09055 100644 --- a/taiga/base/api/viewsets.py +++ b/taiga/base/api/viewsets.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/apps.py b/taiga/base/apps.py index b56aaafb..f5f1879b 100644 --- a/taiga/base/apps.py +++ b/taiga/base/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/connectors/exceptions.py b/taiga/base/connectors/exceptions.py index 0aca19fd..eb47c5db 100644 --- a/taiga/base/connectors/exceptions.py +++ b/taiga/base/connectors/exceptions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py index afddb058..5700e75b 100644 --- a/taiga/base/decorators.py +++ b/taiga/base/decorators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index 104ba896..cc58ee6d 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/fields.py b/taiga/base/fields.py index e45dace8..3f6fcf19 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/filters.py b/taiga/base/filters.py index ea962af0..1cd19e64 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/formats/en/formats.py b/taiga/base/formats/en/formats.py index 5046462e..37d64ee8 100644 --- a/taiga/base/formats/en/formats.py +++ b/taiga/base/formats/en/formats.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/formats/es/formats.py b/taiga/base/formats/es/formats.py index ab561c7f..f6d7dc55 100644 --- a/taiga/base/formats/es/formats.py +++ b/taiga/base/formats/es/formats.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/mails.py b/taiga/base/mails.py index 51be494d..48e6f296 100644 --- a/taiga/base/mails.py +++ b/taiga/base/mails.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py index 53a63a87..631bcd42 100644 --- a/taiga/base/management/commands/test_emails.py +++ b/taiga/base/management/commands/test_emails.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/middleware/cors.py b/taiga/base/middleware/cors.py index 101ff7a2..c7e2c615 100644 --- a/taiga/base/middleware/cors.py +++ b/taiga/base/middleware/cors.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py index 7a2f079e..a57d2eeb 100644 --- a/taiga/base/neighbors.py +++ b/taiga/base/neighbors.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/response.py b/taiga/base/response.py index 5b84123a..82d7794f 100644 --- a/taiga/base/response.py +++ b/taiga/base/response.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/routers.py b/taiga/base/routers.py index 4cf8a6f6..56b80f8e 100644 --- a/taiga/base/routers.py +++ b/taiga/base/routers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/signals/cleanup_files.py b/taiga/base/signals/cleanup_files.py index 0efab210..e2449ce2 100644 --- a/taiga/base/signals/cleanup_files.py +++ b/taiga/base/signals/cleanup_files.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/signals/thumbnails.py b/taiga/base/signals/thumbnails.py index 53bc1bca..e40cba07 100644 --- a/taiga/base/signals/thumbnails.py +++ b/taiga/base/signals/thumbnails.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/status.py b/taiga/base/status.py index 003c771b..c271e030 100644 --- a/taiga/base/status.py +++ b/taiga/base/status.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/storage.py b/taiga/base/storage.py index 84c2a460..a0b962c4 100644 --- a/taiga/base/storage.py +++ b/taiga/base/storage.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/tags.py b/taiga/base/tags.py index 8ba99e59..0e1cd866 100644 --- a/taiga/base/tags.py +++ b/taiga/base/tags.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/throttling.py b/taiga/base/throttling.py index b9cae88d..c0577ce0 100644 --- a/taiga/base/throttling.py +++ b/taiga/base/throttling.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/contenttypes.py b/taiga/base/utils/contenttypes.py index 6b7a8e37..252a3db2 100644 --- a/taiga/base/utils/contenttypes.py +++ b/taiga/base/utils/contenttypes.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py index 2e076021..6569069d 100644 --- a/taiga/base/utils/db.py +++ b/taiga/base/utils/db.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/dicts.py b/taiga/base/utils/dicts.py index 232832ff..23b90f17 100644 --- a/taiga/base/utils/dicts.py +++ b/taiga/base/utils/dicts.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/diff.py b/taiga/base/utils/diff.py index 4b18a071..e08584ae 100644 --- a/taiga/base/utils/diff.py +++ b/taiga/base/utils/diff.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/files.py b/taiga/base/utils/files.py index 72214606..1772e2af 100644 --- a/taiga/base/utils/files.py +++ b/taiga/base/utils/files.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/functions.py b/taiga/base/utils/functions.py index 769b868c..d25d3572 100644 --- a/taiga/base/utils/functions.py +++ b/taiga/base/utils/functions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/iterators.py b/taiga/base/utils/iterators.py index a8b9a88e..19d8c553 100644 --- a/taiga/base/utils/iterators.py +++ b/taiga/base/utils/iterators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/json.py b/taiga/base/utils/json.py index a5e0a477..6e2a0658 100644 --- a/taiga/base/utils/json.py +++ b/taiga/base/utils/json.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/sequence.py b/taiga/base/utils/sequence.py index 009723dc..4e0c3e24 100644 --- a/taiga/base/utils/sequence.py +++ b/taiga/base/utils/sequence.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/signals.py b/taiga/base/utils/signals.py index 863128b1..9dbef497 100644 --- a/taiga/base/utils/signals.py +++ b/taiga/base/utils/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/slug.py b/taiga/base/utils/slug.py index 53c246d9..239366d3 100644 --- a/taiga/base/utils/slug.py +++ b/taiga/base/utils/slug.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/text.py b/taiga/base/utils/text.py index dce1cb55..05447b45 100644 --- a/taiga/base/utils/text.py +++ b/taiga/base/utils/text.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/thumbnails.py b/taiga/base/utils/thumbnails.py index 1337ad84..64ce2d20 100644 --- a/taiga/base/utils/thumbnails.py +++ b/taiga/base/utils/thumbnails.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/base/utils/urls.py b/taiga/base/utils/urls.py index b2ea88d4..36239113 100644 --- a/taiga/base/utils/urls.py +++ b/taiga/base/utils/urls.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/celery.py b/taiga/celery.py index 00daf832..2cafa1b8 100644 --- a/taiga/celery.py +++ b/taiga/celery.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/deferred.py b/taiga/deferred.py index 95d57ab6..30a17d6b 100644 --- a/taiga/deferred.py +++ b/taiga/deferred.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/events/__init__.py b/taiga/events/__init__.py index bcbe54c4..b6017f3b 100644 --- a/taiga/events/__init__.py +++ b/taiga/events/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/events/apps.py b/taiga/events/apps.py index 6b4c6a59..7c9a7680 100644 --- a/taiga/events/apps.py +++ b/taiga/events/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/events/backends/__init__.py b/taiga/events/backends/__init__.py index 72de8147..f3f0d6c3 100644 --- a/taiga/events/backends/__init__.py +++ b/taiga/events/backends/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/events/backends/base.py b/taiga/events/backends/base.py index 0bcaf69e..a9f0e8f2 100644 --- a/taiga/events/backends/base.py +++ b/taiga/events/backends/base.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/events/backends/postgresql.py b/taiga/events/backends/postgresql.py index 4072120a..e239d421 100644 --- a/taiga/events/backends/postgresql.py +++ b/taiga/events/backends/postgresql.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/events/backends/rabbitmq.py b/taiga/events/backends/rabbitmq.py index 2e65163c..829dcf3a 100644 --- a/taiga/events/backends/rabbitmq.py +++ b/taiga/events/backends/rabbitmq.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/events/events.py b/taiga/events/events.py index 394495a9..2df3dd37 100644 --- a/taiga/events/events.py +++ b/taiga/events/events.py @@ -1,4 +1,8 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso # 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 @@ -12,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + import json import collections diff --git a/taiga/events/management/commands/emit_notification_message.py b/taiga/events/management/commands/emit_notification_message.py index ec023bc7..7ae7facc 100644 --- a/taiga/events/management/commands/emit_notification_message.py +++ b/taiga/events/management/commands/emit_notification_message.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/events/middleware.py b/taiga/events/middleware.py index 4d2e349f..1fa5c182 100644 --- a/taiga/events/middleware.py +++ b/taiga/events/middleware.py @@ -1,4 +1,8 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso # 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 @@ -12,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + import threading _local = threading.local() diff --git a/taiga/events/signal_handlers.py b/taiga/events/signal_handlers.py index ac2ec87d..cb2a1250 100644 --- a/taiga/events/signal_handlers.py +++ b/taiga/events/signal_handlers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index f84e263f..bf5cf1a9 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/exceptions.py b/taiga/export_import/exceptions.py index 623d5b24..9b57306f 100644 --- a/taiga/export_import/exceptions.py +++ b/taiga/export_import/exceptions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py index 0b8938bc..3b3ceaf6 100644 --- a/taiga/export_import/management/commands/dump_project.py +++ b/taiga/export_import/management/commands/dump_project.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index 61209862..8a4ca585 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/mixins.py b/taiga/export_import/mixins.py index 8009aac1..e70e674f 100644 --- a/taiga/export_import/mixins.py +++ b/taiga/export_import/mixins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/permissions.py b/taiga/export_import/permissions.py index 22a03ebe..6d69964a 100644 --- a/taiga/export_import/permissions.py +++ b/taiga/export_import/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/renderers.py b/taiga/export_import/renderers.py index acaf7f16..04e28bbd 100644 --- a/taiga/export_import/renderers.py +++ b/taiga/export_import/renderers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index a8856f2f..43acb5af 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/services/__init__.py b/taiga/export_import/services/__init__.py index 8aad0f08..573d7a70 100644 --- a/taiga/export_import/services/__init__.py +++ b/taiga/export_import/services/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py index b9905baf..cc4f8edf 100644 --- a/taiga/export_import/services/render.py +++ b/taiga/export_import/services/render.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index dc521b26..c7888ce4 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index 4e5012d1..8ba61645 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/export_import/throttling.py b/taiga/export_import/throttling.py index 0d00f137..c96d6c17 100644 --- a/taiga/export_import/throttling.py +++ b/taiga/export_import/throttling.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/admin.py b/taiga/external_apps/admin.py index 6047de49..4004c3ea 100644 --- a/taiga/external_apps/admin.py +++ b/taiga/external_apps/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/api.py b/taiga/external_apps/api.py index 45824bc8..931337a8 100644 --- a/taiga/external_apps/api.py +++ b/taiga/external_apps/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/auth_backends.py b/taiga/external_apps/auth_backends.py index f51398d1..47eba944 100644 --- a/taiga/external_apps/auth_backends.py +++ b/taiga/external_apps/auth_backends.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/encryption.py b/taiga/external_apps/encryption.py index 6bf58a23..77422988 100644 --- a/taiga/external_apps/encryption.py +++ b/taiga/external_apps/encryption.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/models.py b/taiga/external_apps/models.py index 979978f4..8ddd069b 100644 --- a/taiga/external_apps/models.py +++ b/taiga/external_apps/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/permissions.py b/taiga/external_apps/permissions.py index 20293ea1..ad37274b 100644 --- a/taiga/external_apps/permissions.py +++ b/taiga/external_apps/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/serializers.py b/taiga/external_apps/serializers.py index 74239724..095465fd 100644 --- a/taiga/external_apps/serializers.py +++ b/taiga/external_apps/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/external_apps/services.py b/taiga/external_apps/services.py index 8c0854db..336b379a 100644 --- a/taiga/external_apps/services.py +++ b/taiga/external_apps/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/__init__.py b/taiga/feedback/__init__.py index 6ed582cb..ba163f3d 100644 --- a/taiga/feedback/__init__.py +++ b/taiga/feedback/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/admin.py b/taiga/feedback/admin.py index ad1cb01a..aac487ae 100644 --- a/taiga/feedback/admin.py +++ b/taiga/feedback/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/api.py b/taiga/feedback/api.py index 8aadbcbb..c477b5eb 100644 --- a/taiga/feedback/api.py +++ b/taiga/feedback/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/apps.py b/taiga/feedback/apps.py index b48a3cfb..6485ac13 100644 --- a/taiga/feedback/apps.py +++ b/taiga/feedback/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/models.py b/taiga/feedback/models.py index bbf845cd..a75c27a1 100644 --- a/taiga/feedback/models.py +++ b/taiga/feedback/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/permissions.py b/taiga/feedback/permissions.py index 312738c7..b813c567 100644 --- a/taiga/feedback/permissions.py +++ b/taiga/feedback/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/routers.py b/taiga/feedback/routers.py index 8e694d0a..9f907520 100644 --- a/taiga/feedback/routers.py +++ b/taiga/feedback/routers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/serializers.py b/taiga/feedback/serializers.py index a25ae8af..1b5f1a3e 100644 --- a/taiga/feedback/serializers.py +++ b/taiga/feedback/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/feedback/services.py b/taiga/feedback/services.py index 65667a36..8aec0cfc 100644 --- a/taiga/feedback/services.py +++ b/taiga/feedback/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/front/sitemaps/__init__.py b/taiga/front/sitemaps/__init__.py index 06af8fe0..abc78ffe 100644 --- a/taiga/front/sitemaps/__init__.py +++ b/taiga/front/sitemaps/__init__.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/base.py b/taiga/front/sitemaps/base.py index ad007b60..952d754d 100644 --- a/taiga/front/sitemaps/base.py +++ b/taiga/front/sitemaps/base.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/generics.py b/taiga/front/sitemaps/generics.py index 241316e1..9ddc51bc 100644 --- a/taiga/front/sitemaps/generics.py +++ b/taiga/front/sitemaps/generics.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/issues.py b/taiga/front/sitemaps/issues.py index bf5694e0..4aeeb100 100644 --- a/taiga/front/sitemaps/issues.py +++ b/taiga/front/sitemaps/issues.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/milestones.py b/taiga/front/sitemaps/milestones.py index 274cbaf8..f0b7345a 100644 --- a/taiga/front/sitemaps/milestones.py +++ b/taiga/front/sitemaps/milestones.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/projects.py b/taiga/front/sitemaps/projects.py index cdfc8cc2..a7e45d50 100644 --- a/taiga/front/sitemaps/projects.py +++ b/taiga/front/sitemaps/projects.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/tasks.py b/taiga/front/sitemaps/tasks.py index eaa599ff..56f3ee7e 100644 --- a/taiga/front/sitemaps/tasks.py +++ b/taiga/front/sitemaps/tasks.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/users.py b/taiga/front/sitemaps/users.py index 5e956dd7..cabe9417 100644 --- a/taiga/front/sitemaps/users.py +++ b/taiga/front/sitemaps/users.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/userstories.py b/taiga/front/sitemaps/userstories.py index 9d66773c..7c89b0a0 100644 --- a/taiga/front/sitemaps/userstories.py +++ b/taiga/front/sitemaps/userstories.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/sitemaps/wiki.py b/taiga/front/sitemaps/wiki.py index 85e03ba0..e0ec43af 100644 --- a/taiga/front/sitemaps/wiki.py +++ b/taiga/front/sitemaps/wiki.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Taiga Agile LLC -# # 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 diff --git a/taiga/front/templatetags/functions.py b/taiga/front/templatetags/functions.py index cb78b567..86a7f813 100644 --- a/taiga/front/templatetags/functions.py +++ b/taiga/front/templatetags/functions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/front/urls.py b/taiga/front/urls.py index e7ea3e0f..ab1cec8c 100644 --- a/taiga/front/urls.py +++ b/taiga/front/urls.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/api.py b/taiga/hooks/api.py index 6590832e..eef06cf8 100644 --- a/taiga/hooks/api.py +++ b/taiga/hooks/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py index aa4c9f63..24fc478c 100644 --- a/taiga/hooks/bitbucket/api.py +++ b/taiga/hooks/bitbucket/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py index 30d92c39..8737aaa7 100644 --- a/taiga/hooks/bitbucket/event_hooks.py +++ b/taiga/hooks/bitbucket/event_hooks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/bitbucket/models.py b/taiga/hooks/bitbucket/models.py index fca83d73..ffdcf2b0 100644 --- a/taiga/hooks/bitbucket/models.py +++ b/taiga/hooks/bitbucket/models.py @@ -1 +1,2 @@ +# -*- coding: utf-8 -*- # This file is needed to load migrations diff --git a/taiga/hooks/bitbucket/services.py b/taiga/hooks/bitbucket/services.py index 9b2b8d21..b2cb29c7 100644 --- a/taiga/hooks/bitbucket/services.py +++ b/taiga/hooks/bitbucket/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/event_hooks.py b/taiga/hooks/event_hooks.py index 931047e0..f4f6d2e8 100644 --- a/taiga/hooks/event_hooks.py +++ b/taiga/hooks/event_hooks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/exceptions.py b/taiga/hooks/exceptions.py index 506e9f60..bbe68cc1 100644 --- a/taiga/hooks/exceptions.py +++ b/taiga/hooks/exceptions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/github/api.py b/taiga/hooks/github/api.py index e021fddf..f3c125ee 100644 --- a/taiga/hooks/github/api.py +++ b/taiga/hooks/github/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py index e280af0a..68e57993 100644 --- a/taiga/hooks/github/event_hooks.py +++ b/taiga/hooks/github/event_hooks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/github/models.py b/taiga/hooks/github/models.py index fca83d73..ffdcf2b0 100644 --- a/taiga/hooks/github/models.py +++ b/taiga/hooks/github/models.py @@ -1 +1,2 @@ +# -*- coding: utf-8 -*- # This file is needed to load migrations diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py index 830406bf..cd244ae3 100644 --- a/taiga/hooks/github/services.py +++ b/taiga/hooks/github/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py index 88067e7a..910ee437 100644 --- a/taiga/hooks/gitlab/api.py +++ b/taiga/hooks/gitlab/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py index 5eb0d478..aff09e2f 100644 --- a/taiga/hooks/gitlab/event_hooks.py +++ b/taiga/hooks/gitlab/event_hooks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/hooks/gitlab/models.py b/taiga/hooks/gitlab/models.py index fca83d73..ffdcf2b0 100644 --- a/taiga/hooks/gitlab/models.py +++ b/taiga/hooks/gitlab/models.py @@ -1 +1,2 @@ +# -*- coding: utf-8 -*- # This file is needed to load migrations diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py index d80fe62c..cd4751fb 100644 --- a/taiga/hooks/gitlab/services.py +++ b/taiga/hooks/gitlab/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/locale/api.py b/taiga/locale/api.py index 8c151730..d9add761 100644 --- a/taiga/locale/api.py +++ b/taiga/locale/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/locale/permissions.py b/taiga/locale/permissions.py index fbcd654b..3146d58e 100644 --- a/taiga/locale/permissions.py +++ b/taiga/locale/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/mdrender/extensions/autolink.py b/taiga/mdrender/extensions/autolink.py index 4413353a..e09898bf 100644 --- a/taiga/mdrender/extensions/autolink.py +++ b/taiga/mdrender/extensions/autolink.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. diff --git a/taiga/mdrender/extensions/automail.py b/taiga/mdrender/extensions/automail.py index ba69eecc..7c98520c 100644 --- a/taiga/mdrender/extensions/automail.py +++ b/taiga/mdrender/extensions/automail.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. diff --git a/taiga/mdrender/extensions/semi_sane_lists.py b/taiga/mdrender/extensions/semi_sane_lists.py index d208e1c4..3a6c4343 100644 --- a/taiga/mdrender/extensions/semi_sane_lists.py +++ b/taiga/mdrender/extensions/semi_sane_lists.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. diff --git a/taiga/mdrender/extensions/spaced_link.py b/taiga/mdrender/extensions/spaced_link.py index 7f619cdb..7c96dda9 100644 --- a/taiga/mdrender/extensions/spaced_link.py +++ b/taiga/mdrender/extensions/spaced_link.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. diff --git a/taiga/mdrender/extensions/strikethrough.py b/taiga/mdrender/extensions/strikethrough.py index c00a09d7..c9517225 100644 --- a/taiga/mdrender/extensions/strikethrough.py +++ b/taiga/mdrender/extensions/strikethrough.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. diff --git a/taiga/mdrender/extensions/target_link.py b/taiga/mdrender/extensions/target_link.py index 77e3685c..b9cff1c3 100644 --- a/taiga/mdrender/extensions/target_link.py +++ b/taiga/mdrender/extensions/target_link.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/mdrender/extensions/wikilinks.py b/taiga/mdrender/extensions/wikilinks.py index 61c36d5c..52363bc1 100644 --- a/taiga/mdrender/extensions/wikilinks.py +++ b/taiga/mdrender/extensions/wikilinks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/mdrender/service.py b/taiga/mdrender/service.py index 226ec525..cc87e25b 100644 --- a/taiga/mdrender/service.py +++ b/taiga/mdrender/service.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/mdrender/templatetags/__init__.py b/taiga/mdrender/templatetags/__init__.py index 8b137891..e69de29b 100644 --- a/taiga/mdrender/templatetags/__init__.py +++ b/taiga/mdrender/templatetags/__init__.py @@ -1 +0,0 @@ - diff --git a/taiga/mdrender/templatetags/functions.py b/taiga/mdrender/templatetags/functions.py index 7186c236..3bb52ebc 100644 --- a/taiga/mdrender/templatetags/functions.py +++ b/taiga/mdrender/templatetags/functions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py index f617da97..48b92ace 100644 --- a/taiga/permissions/choices.py +++ b/taiga/permissions/choices.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/permissions/permissions.py b/taiga/permissions/permissions.py index c2b7699a..0ffefe40 100644 --- a/taiga/permissions/permissions.py +++ b/taiga/permissions/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/permissions/services.py b/taiga/permissions/services.py index 9ed1e8c3..8cb679a1 100644 --- a/taiga/permissions/services.py +++ b/taiga/permissions/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/__init__.py b/taiga/projects/__init__.py index 08a5593c..a4281e96 100644 --- a/taiga/projects/__init__.py +++ b/taiga/projects/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 3349f360..94bfdc42 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/api.py b/taiga/projects/api.py index b37250a5..9c00901c 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py index c8c56bb3..a390b5f5 100644 --- a/taiga/projects/apps.py +++ b/taiga/projects/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/__init__.py b/taiga/projects/attachments/__init__.py index b9d4f659..bdc2814a 100644 --- a/taiga/projects/attachments/__init__.py +++ b/taiga/projects/attachments/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/admin.py b/taiga/projects/attachments/admin.py index ecfa6fed..befbef4f 100644 --- a/taiga/projects/attachments/admin.py +++ b/taiga/projects/attachments/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 481021ed..f7b223e2 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/apps.py b/taiga/projects/attachments/apps.py index 6e1508e1..2f9edae2 100644 --- a/taiga/projects/attachments/apps.py +++ b/taiga/projects/attachments/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/management/commands/generate_sha1.py b/taiga/projects/attachments/management/commands/generate_sha1.py index 8441d127..567788d1 100644 --- a/taiga/projects/attachments/management/commands/generate_sha1.py +++ b/taiga/projects/attachments/management/commands/generate_sha1.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.core.management.base import BaseCommand, CommandError from django.db import transaction diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index daec4a2c..8bbbee16 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py index 0896e506..4e0f5d3e 100644 --- a/taiga/projects/attachments/permissions.py +++ b/taiga/projects/attachments/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index bb15e61e..904498a9 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/attachments/services.py b/taiga/projects/attachments/services.py index ee25c9b5..b3305552 100644 --- a/taiga/projects/attachments/services.py +++ b/taiga/projects/attachments/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py index 15234a6d..75a3630c 100644 --- a/taiga/projects/choices.py +++ b/taiga/projects/choices.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/admin.py b/taiga/projects/custom_attributes/admin.py index 24e070b5..fca94b96 100644 --- a/taiga/projects/custom_attributes/admin.py +++ b/taiga/projects/custom_attributes/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index a11d6e31..9bfc774f 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/choices.py b/taiga/projects/custom_attributes/choices.py index fadcc788..d03ce070 100644 --- a/taiga/projects/custom_attributes/choices.py +++ b/taiga/projects/custom_attributes/choices.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 525fdaff..d7e4e32c 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py index 766f9d42..5771cce4 100644 --- a/taiga/projects/custom_attributes/permissions.py +++ b/taiga/projects/custom_attributes/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 57eefa4f..64a934f5 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/services.py b/taiga/projects/custom_attributes/services.py index 5d65a039..c957c5dc 100644 --- a/taiga/projects/custom_attributes/services.py +++ b/taiga/projects/custom_attributes/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/custom_attributes/signals.py b/taiga/projects/custom_attributes/signals.py index fcb6be8a..72b715a7 100644 --- a/taiga/projects/custom_attributes/signals.py +++ b/taiga/projects/custom_attributes/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py index 3bdb10c8..b3be1a0a 100644 --- a/taiga/projects/filters.py +++ b/taiga/projects/filters.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 3f1ca240..a4c8199e 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/history/choices.py b/taiga/projects/history/choices.py index 19987ae0..411e0f55 100644 --- a/taiga/projects/history/choices.py +++ b/taiga/projects/history/choices.py @@ -1,4 +1,8 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso # 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 diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 34f4139d..9b2dcadc 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/history/mixins.py b/taiga/projects/history/mixins.py index 29294ce8..0a70366d 100644 --- a/taiga/projects/history/mixins.py +++ b/taiga/projects/history/mixins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 39012f60..ee4868b1 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -1,4 +1,8 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso # 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 diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index 54fc59f1..e55ac3c4 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index f231f29c..f1a10481 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 61004471..d4df56a5 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -1,4 +1,8 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso # 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 diff --git a/taiga/projects/history/templatetags/__init__.py b/taiga/projects/history/templatetags/__init__.py index 8b137891..e69de29b 100644 --- a/taiga/projects/history/templatetags/__init__.py +++ b/taiga/projects/history/templatetags/__init__.py @@ -1 +0,0 @@ - diff --git a/taiga/projects/history/templatetags/functions.py b/taiga/projects/history/templatetags/functions.py index 31dd3371..8eefa6c1 100644 --- a/taiga/projects/history/templatetags/functions.py +++ b/taiga/projects/history/templatetags/functions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/__init__.py b/taiga/projects/issues/__init__.py index 37ec56ee..a259bd6b 100644 --- a/taiga/projects/issues/__init__.py +++ b/taiga/projects/issues/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/admin.py b/taiga/projects/issues/admin.py index 2b72e9d5..06be1a03 100644 --- a/taiga/projects/issues/admin.py +++ b/taiga/projects/issues/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index c0ade2f4..b368ec8d 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index 26d58b18..4d0bca19 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index df11f671..89a78051 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index ea823fcc..d86f697c 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index c6810a93..6c2f877e 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index 6a6c5a37..a494b1f4 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/issues/signals.py b/taiga/projects/issues/signals.py index b92418ee..91bb5ebc 100644 --- a/taiga/projects/issues/signals.py +++ b/taiga/projects/issues/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/likes/admin.py b/taiga/projects/likes/admin.py index c18f94d1..bab44a69 100644 --- a/taiga/projects/likes/admin.py +++ b/taiga/projects/likes/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/likes/mixins/serializers.py b/taiga/projects/likes/mixins/serializers.py index 537cef13..84d63b4e 100644 --- a/taiga/projects/likes/mixins/serializers.py +++ b/taiga/projects/likes/mixins/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/likes/mixins/viewsets.py b/taiga/projects/likes/mixins/viewsets.py index 03bf8987..cce443c8 100644 --- a/taiga/projects/likes/mixins/viewsets.py +++ b/taiga/projects/likes/mixins/viewsets.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/likes/models.py b/taiga/projects/likes/models.py index 078f48e3..c363b8c8 100644 --- a/taiga/projects/likes/models.py +++ b/taiga/projects/likes/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/likes/serializers.py b/taiga/projects/likes/serializers.py index 7d28f8f8..6a654705 100644 --- a/taiga/projects/likes/serializers.py +++ b/taiga/projects/likes/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/likes/services.py b/taiga/projects/likes/services.py index cf35746d..617a4da9 100644 --- a/taiga/projects/likes/services.py +++ b/taiga/projects/likes/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index be43df8c..0c355c2f 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/admin.py b/taiga/projects/milestones/admin.py index 37c91b4d..d3f8c994 100644 --- a/taiga/projects/milestones/admin.py +++ b/taiga/projects/milestones/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 1728362b..f109060b 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index bf7bb469..21d85b14 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/permissions.py b/taiga/projects/milestones/permissions.py index 7404feb0..fedb1ee0 100644 --- a/taiga/projects/milestones/permissions.py +++ b/taiga/projects/milestones/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 191b2234..2a52be47 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/services.py b/taiga/projects/milestones/services.py index a717e04f..289c4834 100644 --- a/taiga/projects/milestones/services.py +++ b/taiga/projects/milestones/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py index 39ba2027..3648a672 100644 --- a/taiga/projects/milestones/validators.py +++ b/taiga/projects/milestones/validators.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 as _ from taiga.base.api import serializers diff --git a/taiga/projects/mixins/blocked.py b/taiga/projects/mixins/blocked.py index 6a750825..d7f729cc 100644 --- a/taiga/projects/mixins/blocked.py +++ b/taiga/projects/mixins/blocked.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/mixins/on_destroy.py b/taiga/projects/mixins/on_destroy.py index 1e45353a..61d4728d 100644 --- a/taiga/projects/mixins/on_destroy.py +++ b/taiga/projects/mixins/on_destroy.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/mixins/ordering.py b/taiga/projects/mixins/ordering.py index 6917f1e3..18649c16 100644 --- a/taiga/projects/mixins/ordering.py +++ b/taiga/projects/mixins/ordering.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index edaa0c1a..07a9b683 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 4ff459e0..060fbdb8 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/admin.py b/taiga/projects/notifications/admin.py index 07a70396..5e0bd931 100644 --- a/taiga/projects/notifications/admin.py +++ b/taiga/projects/notifications/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py index 10f3d5a3..cd8f564e 100644 --- a/taiga/projects/notifications/api.py +++ b/taiga/projects/notifications/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/choices.py b/taiga/projects/notifications/choices.py index a6fdfe31..fdf87328 100644 --- a/taiga/projects/notifications/choices.py +++ b/taiga/projects/notifications/choices.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/management/commands/send_notifications.py b/taiga/projects/notifications/management/commands/send_notifications.py index bb259cde..e3eefedc 100644 --- a/taiga/projects/notifications/management/commands/send_notifications.py +++ b/taiga/projects/notifications/management/commands/send_notifications.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index fb3cada7..ee1d59f8 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/models.py b/taiga/projects/notifications/models.py index 9c36fe75..4a2f56da 100644 --- a/taiga/projects/notifications/models.py +++ b/taiga/projects/notifications/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/permissions.py b/taiga/projects/notifications/permissions.py index 0f65e159..9515f393 100644 --- a/taiga/projects/notifications/permissions.py +++ b/taiga/projects/notifications/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/serializers.py b/taiga/projects/notifications/serializers.py index ed93dce7..1578b7be 100644 --- a/taiga/projects/notifications/serializers.py +++ b/taiga/projects/notifications/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 27e4b5ae..4a26545b 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py index 991228b0..00b98d63 100644 --- a/taiga/projects/notifications/utils.py +++ b/taiga/projects/notifications/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/notifications/validators.py b/taiga/projects/notifications/validators.py index 010ef0df..851cc309 100644 --- a/taiga/projects/notifications/validators.py +++ b/taiga/projects/notifications/validators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/occ/__init__.py b/taiga/projects/occ/__init__.py index a66f1b99..248c7af9 100644 --- a/taiga/projects/occ/__init__.py +++ b/taiga/projects/occ/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/occ/mixins.py b/taiga/projects/occ/mixins.py index b3ccfa32..7e837422 100644 --- a/taiga/projects/occ/mixins.py +++ b/taiga/projects/occ/mixins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index ba4f3260..c43e842f 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 0775bc72..42d7f5a6 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py index 3bc63b1c..40aea018 100644 --- a/taiga/projects/references/models.py +++ b/taiga/projects/references/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/references/permissions.py b/taiga/projects/references/permissions.py index baa91d8c..9baa2a5c 100644 --- a/taiga/projects/references/permissions.py +++ b/taiga/projects/references/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/references/sequences.py b/taiga/projects/references/sequences.py index 5bcb89af..711b5555 100644 --- a/taiga/projects/references/sequences.py +++ b/taiga/projects/references/sequences.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/references/serializers.py b/taiga/projects/references/serializers.py index 3aac3fa2..fb9ad177 100644 --- a/taiga/projects/references/serializers.py +++ b/taiga/projects/references/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/references/services.py b/taiga/projects/references/services.py index 7369da53..6469937d 100644 --- a/taiga/projects/references/services.py +++ b/taiga/projects/references/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 7096919a..1b271590 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index d33d51e9..fb3cb9c5 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py index 91b33d34..48e85218 100644 --- a/taiga/projects/services/bulk_update_order.py +++ b/taiga/projects/services/bulk_update_order.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/filters.py b/taiga/projects/services/filters.py index 96347cf5..0236e82b 100644 --- a/taiga/projects/services/filters.py +++ b/taiga/projects/services/filters.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/invitations.py b/taiga/projects/services/invitations.py index 50e1e01e..5e772bd2 100644 --- a/taiga/projects/services/invitations.py +++ b/taiga/projects/services/invitations.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.apps import apps from django.conf import settings diff --git a/taiga/projects/services/logo.py b/taiga/projects/services/logo.py index cbb0bf8e..1f4f7a38 100644 --- a/taiga/projects/services/logo.py +++ b/taiga/projects/services/logo.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py index 039ffd32..4de432ab 100644 --- a/taiga/projects/services/members.py +++ b/taiga/projects/services/members.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/modules_config.py b/taiga/projects/services/modules_config.py index b850be56..d48a7094 100644 --- a/taiga/projects/services/modules_config.py +++ b/taiga/projects/services/modules_config.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py index 43309851..b1befaf7 100644 --- a/taiga/projects/services/projects.py +++ b/taiga/projects/services/projects.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py index 0972dc6b..891be36b 100644 --- a/taiga/projects/services/stats.py +++ b/taiga/projects/services/stats.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py index 90d44efa..9b9aa962 100644 --- a/taiga/projects/services/tags_colors.py +++ b/taiga/projects/services/tags_colors.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/services/transfer.py b/taiga/projects/services/transfer.py index bef18577..aa9ea934 100644 --- a/taiga/projects/services/transfer.py +++ b/taiga/projects/services/transfer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py index 32da1176..ca5d7094 100644 --- a/taiga/projects/signals.py +++ b/taiga/projects/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/__init__.py b/taiga/projects/tasks/__init__.py index cdea243c..78f3e8ae 100644 --- a/taiga/projects/tasks/__init__.py +++ b/taiga/projects/tasks/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/admin.py b/taiga/projects/tasks/admin.py index b0f7683e..281cdb11 100644 --- a/taiga/projects/tasks/admin.py +++ b/taiga/projects/tasks/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 6bb2a1a3..d991b39b 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index ad0209a0..616854f6 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index a1e945f1..30406387 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 8dfafc41..8cf40dd7 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index 3f159379..a7c1c2a8 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 2736e317..98f9d4a1 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/signals.py b/taiga/projects/tasks/signals.py index 5f1ae65a..4b9f7af1 100644 --- a/taiga/projects/tasks/signals.py +++ b/taiga/projects/tasks/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index c3db4b1e..4a100779 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 as _ from taiga.base.api import serializers diff --git a/taiga/projects/translations.py b/taiga/projects/translations.py index e7a728c2..3f22f6e0 100644 --- a/taiga/projects/translations.py +++ b/taiga/projects/translations.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/__init__.py b/taiga/projects/userstories/__init__.py index 0cb5907d..e73d4118 100644 --- a/taiga/projects/userstories/__init__.py +++ b/taiga/projects/userstories/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/admin.py b/taiga/projects/userstories/admin.py index aa1f8a28..50c8bb7b 100644 --- a/taiga/projects/userstories/admin.py +++ b/taiga/projects/userstories/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 1b0b8035..eb05fe31 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index 58e0a65c..ef3d5df5 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 4a046524..86332b46 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index 51c6f01a..6b46f72c 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 0a856e8a..0d6eab6d 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index c1884228..5ce47635 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py index b5afb105..11638595 100644 --- a/taiga/projects/userstories/signals.py +++ b/taiga/projects/userstories/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 68e4940e..5ad5e7f4 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index 8b7324bc..05866b66 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/admin.py b/taiga/projects/votes/admin.py index 7c6241fb..d7a040e5 100644 --- a/taiga/projects/votes/admin.py +++ b/taiga/projects/votes/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py index c206ee6d..1a6faeb2 100644 --- a/taiga/projects/votes/mixins/serializers.py +++ b/taiga/projects/votes/mixins/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py index 2fce2d60..2456e375 100644 --- a/taiga/projects/votes/mixins/viewsets.py +++ b/taiga/projects/votes/mixins/viewsets.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/models.py b/taiga/projects/votes/models.py index 03c8976f..cfc0947e 100644 --- a/taiga/projects/votes/models.py +++ b/taiga/projects/votes/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py index 78fc94c4..eb47c9ef 100644 --- a/taiga/projects/votes/serializers.py +++ b/taiga/projects/votes/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/services.py b/taiga/projects/votes/services.py index 466855b8..38c3b93a 100644 --- a/taiga/projects/votes/services.py +++ b/taiga/projects/votes/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py index a7ee5bfb..291ee284 100644 --- a/taiga/projects/votes/utils.py +++ b/taiga/projects/votes/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/wiki/admin.py b/taiga/projects/wiki/admin.py index f64dc7da..73834938 100644 --- a/taiga/projects/wiki/admin.py +++ b/taiga/projects/wiki/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index ea014233..2ee75b14 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index c3e20e4e..659e51f0 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py index e60458c7..0afea036 100644 --- a/taiga/projects/wiki/permissions.py +++ b/taiga/projects/wiki/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py index 08b58336..16de19df 100644 --- a/taiga/projects/wiki/serializers.py +++ b/taiga/projects/wiki/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/routers.py b/taiga/routers.py index fdcba0b8..66e1b9f7 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/searches/api.py b/taiga/searches/api.py index f10554a8..37f9c7f3 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py index 1938b5d3..edc2d1ca 100644 --- a/taiga/searches/serializers.py +++ b/taiga/searches/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/searches/services.py b/taiga/searches/services.py index 38367c7e..f393844f 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/stats/__init__.py b/taiga/stats/__init__.py index c472cad5..b393a91f 100644 --- a/taiga/stats/__init__.py +++ b/taiga/stats/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/stats/api.py b/taiga/stats/api.py index 301b056e..5efeed1f 100644 --- a/taiga/stats/api.py +++ b/taiga/stats/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/stats/apps.py b/taiga/stats/apps.py index edb0e90e..7e4d9729 100644 --- a/taiga/stats/apps.py +++ b/taiga/stats/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/stats/permissions.py b/taiga/stats/permissions.py index 369a7000..83d8aca7 100644 --- a/taiga/stats/permissions.py +++ b/taiga/stats/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/stats/routers.py b/taiga/stats/routers.py index 6dbb4e09..4623f3ca 100644 --- a/taiga/stats/routers.py +++ b/taiga/stats/routers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/stats/services.py b/taiga/stats/services.py index 45ae60e5..da551016 100644 --- a/taiga/stats/services.py +++ b/taiga/stats/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Taiga Agile LLC # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/taiga/timeline/__init__.py b/taiga/timeline/__init__.py index 05b04f76..2ce879cf 100644 --- a/taiga/timeline/__init__.py +++ b/taiga/timeline/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index 82dea784..b0bf8e13 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index c9df776d..d3eee80e 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py b/taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py index 516bb343..2f5f581c 100644 --- a/taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py +++ b/taiga/timeline/management/commands/_clear_unnecessary_new_membership_entries.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py index 726664f9..07290281 100644 --- a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py +++ b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py index 7d39765f..090cf5f6 100644 --- a/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py +++ b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py index 6214d129..947a7418 100644 --- a/taiga/timeline/management/commands/rebuild_timeline.py +++ b/taiga/timeline/management/commands/rebuild_timeline.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py index 146d5a1b..2f2ae1b5 100644 --- a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py +++ b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index 73eef400..c71188f7 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/permissions.py b/taiga/timeline/permissions.py index f2753869..61a20130 100644 --- a/taiga/timeline/permissions.py +++ b/taiga/timeline/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index d4a1563c..a6be6944 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 11517542..7aacd4e7 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index c0f1dffa..a2611769 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py index cf6223ab..cff785ad 100644 --- a/taiga/timeline/timeline_implementations.py +++ b/taiga/timeline/timeline_implementations.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/urls.py b/taiga/urls.py index 23afa70f..110285f4 100644 --- a/taiga/urls.py +++ b/taiga/urls.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/admin.py b/taiga/users/admin.py index d236fe9f..1d03d0cf 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/api.py b/taiga/users/api.py index 96e2742d..a02e1576 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/filters.py b/taiga/users/filters.py index f93cbfd5..4e4dc116 100644 --- a/taiga/users/filters.py +++ b/taiga/users/filters.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/forms.py b/taiga/users/forms.py index 4cf74f35..1d466f67 100644 --- a/taiga/users/forms.py +++ b/taiga/users/forms.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/gravatar.py b/taiga/users/gravatar.py index ec94f267..7793e59d 100644 --- a/taiga/users/gravatar.py +++ b/taiga/users/gravatar.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/models.py b/taiga/users/models.py index c1cf4a3b..2d8fcb33 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py index c884edf1..7edca716 100644 --- a/taiga/users/permissions.py +++ b/taiga/users/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index d36ba768..97aeafca 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/services.py b/taiga/users/services.py index 7e210275..5397c46b 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/signals.py b/taiga/users/signals.py index 85be934b..c3fc5c38 100644 --- a/taiga/users/signals.py +++ b/taiga/users/signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/users/validators.py b/taiga/users/validators.py index f6dc9656..477342de 100644 --- a/taiga/users/validators.py +++ b/taiga/users/validators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py index 9814de58..62575d2b 100644 --- a/taiga/userstorage/api.py +++ b/taiga/userstorage/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/userstorage/filters.py b/taiga/userstorage/filters.py index 3c977c5d..479c0220 100644 --- a/taiga/userstorage/filters.py +++ b/taiga/userstorage/filters.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/userstorage/models.py b/taiga/userstorage/models.py index 2760d43a..9888ea7f 100644 --- a/taiga/userstorage/models.py +++ b/taiga/userstorage/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/userstorage/permissions.py b/taiga/userstorage/permissions.py index e93c6bc4..0b7083eb 100644 --- a/taiga/userstorage/permissions.py +++ b/taiga/userstorage/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/userstorage/serializers.py b/taiga/userstorage/serializers.py index 682e7acf..5fd97692 100644 --- a/taiga/userstorage/serializers.py +++ b/taiga/userstorage/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/__init__.py b/taiga/webhooks/__init__.py index 6309b18e..71041013 100644 --- a/taiga/webhooks/__init__.py +++ b/taiga/webhooks/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index 426537dc..f15021a0 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/apps.py b/taiga/webhooks/apps.py index 9fff8e9b..52e39eb0 100644 --- a/taiga/webhooks/apps.py +++ b/taiga/webhooks/apps.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/models.py b/taiga/webhooks/models.py index 3fa9e86a..53969525 100644 --- a/taiga/webhooks/models.py +++ b/taiga/webhooks/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/permissions.py b/taiga/webhooks/permissions.py index 82ca5bd7..f16cb76c 100644 --- a/taiga/webhooks/permissions.py +++ b/taiga/webhooks/permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index a2553b69..68d6a8eb 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/signal_handlers.py b/taiga/webhooks/signal_handlers.py index 415d4fcf..d1c10ba1 100644 --- a/taiga/webhooks/signal_handlers.py +++ b/taiga/webhooks/signal_handlers.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index 7427fe6e..7990b928 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2013 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/taiga/wsgi.py b/taiga/wsgi.py index ada27df2..356c63ef 100644 --- a/taiga/wsgi.py +++ b/taiga/wsgi.py @@ -1,3 +1,22 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + """ WSGI config for taiga project. diff --git a/tests/conftest.py b/tests/conftest.py index 37e495d6..76d2540a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/factories.py b/tests/factories.py index 2f1ddccc..132b007f 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/fixtures.py b/tests/fixtures.py index bdb0fa87..aaeabb42 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/resources_permissions/test_application_tokens_resources.py b/tests/integration/resources_permissions/test_application_tokens_resources.py index cc4e3a5d..5f6d27e7 100644 --- a/tests/integration/resources_permissions/test_application_tokens_resources.py +++ b/tests/integration/resources_permissions/test_application_tokens_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py index 357b13a7..3fdd990a 100644 --- a/tests/integration/resources_permissions/test_attachment_resources.py +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from django.core.files.uploadedfile import SimpleUploadedFile from django.test.client import MULTIPART_CONTENT diff --git a/tests/integration/resources_permissions/test_auth_resources.py b/tests/integration/resources_permissions/test_auth_resources.py index 0cc4cedc..18684d4e 100644 --- a/tests/integration/resources_permissions/test_auth_resources.py +++ b/tests/integration/resources_permissions/test_auth_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_feedback.py b/tests/integration/resources_permissions/test_feedback.py index c3a8d51e..650f6b07 100644 --- a/tests/integration/resources_permissions/test_feedback.py +++ b/tests/integration/resources_permissions/test_feedback.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from tests import factories as f diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index 62ba4e77..c466a9db 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from django.utils import timezone diff --git a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py index 60b57eed..1ec28157 100644 --- a/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_issues_custom_attributes_resource.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 536b4422..922b21e8 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index 56f072f4..d3567ffb 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py index fc1de642..e9403787 100644 --- a/tests/integration/resources_permissions/test_modules_resources.py +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index c26e51b5..0115143a 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index a2ec130e..78c55324 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from django.apps import apps diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index c8634c46..137983e9 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index d72e7bc2..bc8c9ac4 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_storage_resources.py b/tests/integration/resources_permissions/test_storage_resources.py index 4bc6de2b..ebc1eb8a 100644 --- a/tests/integration/resources_permissions/test_storage_resources.py +++ b/tests/integration/resources_permissions/test_storage_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py index 2262a222..1efe9a8d 100644 --- a/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_tasks_custom_attributes_resource.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 63a1d63e..3e5f1824 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py index ed68f67b..1a63db0c 100644 --- a/tests/integration/resources_permissions/test_timelines_resources.py +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py index a6604988..394dd1f8 100644 --- a/tests/integration/resources_permissions/test_users_resources.py +++ b/tests/integration/resources_permissions/test_users_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from tempfile import NamedTemporaryFile from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py index 64e7324a..077219ce 100644 --- a/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py +++ b/tests/integration/resources_permissions/test_userstories_custom_attributes_resource.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index cb5f78ff..55a97c90 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py index 5712b5bc..dd10f04c 100644 --- a/tests/integration/resources_permissions/test_webhooks_resources.py +++ b/tests/integration/resources_permissions/test_webhooks_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index e981aa75..0026e263 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/test_application_tokens.py b/tests/integration/test_application_tokens.py index da381179..b34076b2 100644 --- a/tests/integration/test_application_tokens.py +++ b/tests/integration/test_application_tokens.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from taiga.external_apps import encryption diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py index 3dee3464..94e8e3cf 100644 --- a/tests/integration/test_attachments.py +++ b/tests/integration/test_attachments.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from django.core.urlresolvers import reverse diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 2bfaef9b..5781c58a 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_custom_attributes_issues.py b/tests/integration/test_custom_attributes_issues.py index 7df2a233..50b1a696 100644 --- a/tests/integration/test_custom_attributes_issues.py +++ b/tests/integration/test_custom_attributes_issues.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_custom_attributes_tasks.py b/tests/integration/test_custom_attributes_tasks.py index 9fb1131c..0f6fa3a4 100644 --- a/tests/integration/test_custom_attributes_tasks.py +++ b/tests/integration/test_custom_attributes_tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_custom_attributes_user_stories.py b/tests/integration/test_custom_attributes_user_stories.py index c6a27499..9358c40f 100644 --- a/tests/integration/test_custom_attributes_user_stories.py +++ b/tests/integration/test_custom_attributes_user_stories.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py index 29578c5f..c8727ae8 100644 --- a/tests/integration/test_exporter_api.py +++ b/tests/integration/test_exporter_api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_fan_projects.py b/tests/integration/test_fan_projects.py index 31e73582..680872d2 100644 --- a/tests/integration/test_fan_projects.py +++ b/tests/integration/test_fan_projects.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_feedback.py b/tests/integration/test_feedback.py index 4120e59e..777c65c8 100644 --- a/tests/integration/test_feedback.py +++ b/tests/integration/test_feedback.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from tests import factories as f diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index 5d936c0d..f53c03b9 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 23d5fc18..4b278f38 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest import urllib diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 9bede876..f472f479 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from unittest import mock diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index 14156b37..bf86a80c 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from unittest import mock diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index aff155ff..490ba83b 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 14b01c60..16fc156f 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import uuid import csv diff --git a/tests/integration/test_mdrender.py b/tests/integration/test_mdrender.py index 169230db..64a5182e 100644 --- a/tests/integration/test_mdrender.py +++ b/tests/integration/test_mdrender.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index c878bd45..580378f5 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from unittest import mock from django.core.urlresolvers import reverse diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index 4934c324..ad28cc16 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_models.py b/tests/integration/test_models.py index 5b6547b8..b0926901 100644 --- a/tests/integration/test_models.py +++ b/tests/integration/test_models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from .. import factories as f diff --git a/tests/integration/test_neighbors.py b/tests/integration/test_neighbors.py index 5a416c1c..7f00a55b 100644 --- a/tests/integration/test_neighbors.py +++ b/tests/integration/test_neighbors.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 4d341086..71126b0f 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_occ.py b/tests/integration/test_occ.py index 580f6733..ccb9013f 100644 --- a/tests/integration/test_occ.py +++ b/tests/integration/test_occ.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py index 7ec25929..d40e6afc 100644 --- a/tests/integration/test_permissions.py +++ b/tests/integration/test_permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from taiga.permissions import services, choices diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index fbc09722..376fb9a6 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from django.core.urlresolvers import reverse from django.conf import settings from django.core.files import File diff --git a/tests/integration/test_references_sequences.py b/tests/integration/test_references_sequences.py index 6c856037..2c38900e 100644 --- a/tests/integration/test_references_sequences.py +++ b/tests/integration/test_references_sequences.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_roles.py b/tests/integration/test_roles.py index 7dc978f5..e3b47f7d 100644 --- a/tests/integration/test_roles.py +++ b/tests/integration/test_roles.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index 30c3247b..342b14ea 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py index df68970b..e85f22ab 100644 --- a/tests/integration/test_stats.py +++ b/tests/integration/test_stats.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from .. import factories as f diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index ee897b5f..3323e84f 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import uuid import csv diff --git a/tests/integration/test_throwttling.py b/tests/integration/test_throwttling.py index e231188f..ad2949f4 100644 --- a/tests/integration/test_throwttling.py +++ b/tests/integration/test_throwttling.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 8c8e0182..41376f77 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_totals_projects.py b/tests/integration/test_totals_projects.py index c9bc14b3..e46c1b21 100644 --- a/tests/integration/test_totals_projects.py +++ b/tests/integration/test_totals_projects.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_us_autoclosing.py b/tests/integration/test_us_autoclosing.py index aec566ed..13ba78d2 100644 --- a/tests/integration/test_us_autoclosing.py +++ b/tests/integration/test_us_autoclosing.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index be48c553..d65b2451 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from tempfile import NamedTemporaryFile diff --git a/tests/integration/test_userstorage_api.py b/tests/integration/test_userstorage_api.py index 3c754cad..b1b9ec16 100644 --- a/tests/integration/test_userstorage_api.py +++ b/tests/integration/test_userstorage_api.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index bcb0a618..842f3089 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import copy import uuid import csv diff --git a/tests/integration/test_vote_issues.py b/tests/integration/test_vote_issues.py index b6f8e925..6a4a1bba 100644 --- a/tests/integration/test_vote_issues.py +++ b/tests/integration/test_vote_issues.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_vote_tasks.py b/tests/integration/test_vote_tasks.py index ca3414e6..c369e268 100644 --- a/tests/integration/test_vote_tasks.py +++ b/tests/integration/test_vote_tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_vote_userstories.py b/tests/integration/test_vote_userstories.py index b8caa01b..11ab7532 100644 --- a/tests/integration/test_vote_userstories.py +++ b/tests/integration/test_vote_userstories.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_votes.py b/tests/integration/test_votes.py index d486ed13..68d8cc4d 100644 --- a/tests/integration/test_votes.py +++ b/tests/integration/test_votes.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_watch_issues.py b/tests/integration/test_watch_issues.py index fc22f32c..6d353012 100644 --- a/tests/integration/test_watch_issues.py +++ b/tests/integration/test_watch_issues.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_watch_milestones.py b/tests/integration/test_watch_milestones.py index da17f408..76082db4 100644 --- a/tests/integration/test_watch_milestones.py +++ b/tests/integration/test_watch_milestones.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_watch_projects.py b/tests/integration/test_watch_projects.py index 5a77f086..f6caafb3 100644 --- a/tests/integration/test_watch_projects.py +++ b/tests/integration/test_watch_projects.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_watch_tasks.py b/tests/integration/test_watch_tasks.py index 38ddd40b..b8f12d23 100644 --- a/tests/integration/test_watch_tasks.py +++ b/tests/integration/test_watch_tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_watch_userstories.py b/tests/integration/test_watch_userstories.py index 66ae4e0c..789f6fbe 100644 --- a/tests/integration/test_watch_userstories.py +++ b/tests/integration/test_watch_userstories.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_watch_wikipages.py b/tests/integration/test_watch_wikipages.py index 6940368d..af59d63d 100644 --- a/tests/integration/test_watch_wikipages.py +++ b/tests/integration/test_watch_wikipages.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py index 95f6a4a2..491ec5b4 100644 --- a/tests/integration/test_webhooks_issues.py +++ b/tests/integration/test_webhooks_issues.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_webhooks_milestones.py b/tests/integration/test_webhooks_milestones.py index f720df2f..7d779350 100644 --- a/tests/integration/test_webhooks_milestones.py +++ b/tests/integration/test_webhooks_milestones.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_webhooks_signals.py b/tests/integration/test_webhooks_signals.py index cf9996ca..1db818e6 100644 --- a/tests/integration/test_webhooks_signals.py +++ b/tests/integration/test_webhooks_signals.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_webhooks_tasks.py b/tests/integration/test_webhooks_tasks.py index ba5eda8d..bb9b7d14 100644 --- a/tests/integration/test_webhooks_tasks.py +++ b/tests/integration/test_webhooks_tasks.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_webhooks_userstories.py b/tests/integration/test_webhooks_userstories.py index 716697ce..1580df57 100644 --- a/tests/integration/test_webhooks_userstories.py +++ b/tests/integration/test_webhooks_userstories.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/integration/test_webhooks_wikipages.py b/tests/integration/test_webhooks_wikipages.py index 5d10f233..0e46108c 100644 --- a/tests/integration/test_webhooks_wikipages.py +++ b/tests/integration/test_webhooks_wikipages.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/models.py b/tests/models.py index 366a5269..9583b8c0 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 1890cf84..da0d7085 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_base_api_permissions.py b/tests/unit/test_base_api_permissions.py index b858252f..a76d4359 100644 --- a/tests/unit/test_base_api_permissions.py +++ b/tests/unit/test_base_api_permissions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_deferred.py b/tests/unit/test_deferred.py index bd2a7499..832c4b0b 100644 --- a/tests/unit/test_deferred.py +++ b/tests/unit/test_deferred.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py index 6a4a3ff0..546814a8 100644 --- a/tests/unit/test_export.py +++ b/tests/unit/test_export.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_gravatar.py b/tests/unit/test_gravatar.py index 9244d934..b6246fa8 100644 --- a/tests/unit/test_gravatar.py +++ b/tests/unit/test_gravatar.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_mdrender.py b/tests/unit/test_mdrender.py index 0ce3e5e9..90bbe001 100644 --- a/tests/unit/test_mdrender.py +++ b/tests/unit/test_mdrender.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index 3e656caa..26d7ea41 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pytest from .. import factories as f diff --git a/tests/unit/test_slug.py b/tests/unit/test_slug.py index 9cb4ef5f..1b9de84d 100644 --- a/tests/unit/test_slug.py +++ b/tests/unit/test_slug.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_timeline.py b/tests/unit/test_timeline.py index 18c679e5..c96d3ee8 100644 --- a/tests/unit/test_timeline.py +++ b/tests/unit/test_timeline.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_tokens.py b/tests/unit/test_tokens.py index 53c98c3f..57955827 100644 --- a/tests/unit/test_tokens.py +++ b/tests/unit/test_tokens.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 2cabc548..c564b094 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán diff --git a/tests/utils.py b/tests/utils.py index f16e49fe..2a8a4855 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (C) 2014-2016 Andrey Antukh # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán From f3ae8c73e4cb4da2f5ca7d2576c6a6d7ee242cf8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 31 May 2016 08:58:42 +0200 Subject: [PATCH 030/261] Issue 4257: Mutiple configured webhhoks trigger on the same url --- taiga/webhooks/signal_handlers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/taiga/webhooks/signal_handlers.py b/taiga/webhooks/signal_handlers.py index d1c10ba1..5186454a 100644 --- a/taiga/webhooks/signal_handlers.py +++ b/taiga/webhooks/signal_handlers.py @@ -67,10 +67,18 @@ def on_new_history_entry(sender, instance, created, **kwargs): by = instance.owner date = timezone.now() + webhooks_args = [] for webhook in webhooks: args = [webhook["id"], webhook["url"], webhook["key"], by, date, obj] + extra_args + webhooks_args.append(args) + + connection.on_commit(lambda: _execute_task(task, webhooks_args)) + + +def _execute_task(task, webhooks_args): + for webhook_args in webhooks_args: if settings.CELERY_ENABLED: - connection.on_commit(lambda: task.delay(*args)) + task.delay(*webhook_args) else: - connection.on_commit(lambda: task(*args)) + task(*webhook_args) From 03674f8962fc3ae5fccf74166270013f31986a0b Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 31 May 2016 12:38:03 +0200 Subject: [PATCH 031/261] Improving project deletion --- taiga/projects/choices.py | 5 ++++- taiga/projects/services/projects.py | 2 ++ tests/integration/test_projects.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/taiga/projects/choices.py b/taiga/projects/choices.py index 75a3630c..d1458c44 100644 --- a/taiga/projects/choices.py +++ b/taiga/projects/choices.py @@ -29,8 +29,11 @@ VIDEOCONFERENCES_CHOICES = ( BLOCKED_BY_NONPAYMENT = "blocked-by-nonpayment" BLOCKED_BY_STAFF = "blocked-by-staff" BLOCKED_BY_OWNER_LEAVING = "blocked-by-owner-leaving" +BLOCKED_BY_DELETING = "blocked-by-deleting" + BLOCKING_CODES = [ (BLOCKED_BY_NONPAYMENT, _("This project is blocked due to payment failure")), (BLOCKED_BY_STAFF, _("This project is blocked by admin staff")), - (BLOCKED_BY_OWNER_LEAVING, _("This project is blocked because the owner left")) + (BLOCKED_BY_OWNER_LEAVING, _("This project is blocked because the owner left")), + (BLOCKED_BY_DELETING, _("This project is blocked while it's deleted")) ] diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py index b1befaf7..49abc2e5 100644 --- a/taiga/projects/services/projects.py +++ b/taiga/projects/services/projects.py @@ -19,6 +19,7 @@ from django.apps import apps from django.utils.translation import ugettext as _ from taiga.celery import app +from .. import choices ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships' ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships' @@ -158,6 +159,7 @@ def check_if_project_is_out_of_owner_limits(project): def orphan_project(project): project.memberships.filter(user=project.owner).delete() project.owner = None + project.blocked_code = choices.BLOCKED_BY_DELETING project.save() diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 376fb9a6..9f91cd21 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -10,6 +10,7 @@ from taiga.projects.services import stats as stats_services from taiga.projects.history.services import take_snapshot from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.models import Project +from taiga.projects.choices import BLOCKED_BY_DELETING from .. import factories as f from ..utils import DUMMY_BMP_DATA @@ -1835,6 +1836,7 @@ def test_delete_project_with_celery_enabled(client, settings): project = Project.objects.get(id=project.id) assert project.owner == None assert project.memberships.count() == 0 + assert project.blocked_code == BLOCKED_BY_DELETING delete_project_mock.delay.assert_called_once_with(project.id) From 6da3d8d30f70f205513db2414bee2f6963e2954c Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 31 May 2016 08:02:30 +0200 Subject: [PATCH 032/261] Improving asynch timeline generation --- taiga/timeline/apps.py | 4 ++-- taiga/timeline/service.py | 22 ++++++++++++++++++-- taiga/timeline/signals.py | 33 ++++++++---------------------- tests/integration/test_timeline.py | 29 ++++---------------------- 4 files changed, 35 insertions(+), 53 deletions(-) diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index d3eee80e..7b193552 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -32,9 +32,9 @@ class TimelineAppConfig(AppConfig): signals.post_save.connect(handlers.on_new_history_entry, sender=apps.get_model("history", "HistoryEntry"), dispatch_uid="timeline") - signals.pre_save.connect(handlers.create_membership_push_to_timeline, + signals.post_save.connect(handlers.create_membership_push_to_timeline, sender=apps.get_model("projects", "Membership")) - signals.post_delete.connect(handlers.delete_membership_push_to_timeline, + signals.pre_delete.connect(handlers.delete_membership_push_to_timeline, sender=apps.get_model("projects", "Membership")) signals.post_save.connect(handlers.create_user_push_to_timeline, sender=get_user_model()) diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 7aacd4e7..f99f795e 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -17,6 +17,7 @@ # along with this program. If not, see . from django.apps import apps +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db.models import Model from django.db.models import Q @@ -89,10 +90,27 @@ def _push_to_timeline(objects, instance:object, event_type:str, created_datetime @app.task -def push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}): - if project is not None: +def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type, created_datetime, extra_data={}): + ObjModel = apps.get_model(obj_app_label, obj_model_name) + try: + obj = ObjModel.objects.get(id=obj_id) + except ObjModel.DoesNotExist: + return + + try: + user = get_user_model().objects.get(id=user_id) + except get_user_model().DoesNotExist: + return + + if project_id is not None: # Actions related with a project + projectModel = apps.get_model("projects", "Project") + try: + project = projectModel.objects.get(id=project_id) + except projectModel.DoesNotExist: + return + ## Project timeline _push_to_timeline(project, obj, event_type, created_datetime, namespace=build_project_namespace(project), diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index a2611769..9fc601da 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -18,6 +18,7 @@ from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType from django.utils import timezone from django.utils.translation import ugettext as _ @@ -29,11 +30,14 @@ from taiga.timeline.service import (push_to_timelines, extract_user_info) -def _push_to_timelines(*args, **kwargs): +def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}): + project_id = None if project is None else project.id + + ct = ContentType.objects.get_for_model(obj) if settings.CELERY_ENABLED: - push_to_timelines.delay(*args, **kwargs) + push_to_timelines.delay(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data) else: - push_to_timelines(*args, **kwargs) + push_to_timelines(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data) def _clean_description_fields(values_diff): @@ -89,7 +93,7 @@ def on_new_history_entry(sender, instance, created, **kwargs): _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data=extra_data) -def create_membership_push_to_timeline(sender, instance, **kwargs): +def create_membership_push_to_timeline(sender, instance, created, **kwargs): """ Creating new membership with associated user. If the user is the project owner we don't do anything because that info will be shown in created project timeline entry @@ -99,29 +103,10 @@ def create_membership_push_to_timeline(sender, instance, **kwargs): """ # We shown in created project timeline entry - if not instance.pk and instance.user and instance.user != instance.project.owner: + if created and instance.user and instance.user != instance.project.owner: created_datetime = instance.created_at _push_to_timelines(instance.project, instance.user, instance, "create", created_datetime) - # Updating existing membership - elif instance.pk: - try: - prev_instance = sender.objects.get(pk=instance.pk) - if instance.user != prev_instance.user: - created_datetime = timezone.now() - # The new member - _push_to_timelines(instance.project, instance.user, instance, "create", created_datetime) - # If we are updating the old user is removed from project - if prev_instance.user: - _push_to_timelines(instance.project, - prev_instance.user, - prev_instance, - "delete", - created_datetime) - except sender.DoesNotExist: - # This happens with some tests, when a membership is created with a concrete id - pass - def delete_membership_push_to_timeline(sender, instance, **kwargs): if instance.user: diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 41376f77..e0353d15 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -351,29 +351,6 @@ def test_update_wiki_page_timeline(): assert user_watcher_timeline[0].data["values_diff"]["slug"][1] == "test wiki page timeline updated" -def test_update_membership_timeline(): - user_1 = factories.UserFactory.create() - user_2 = factories.UserFactory.create() - membership = factories.MembershipFactory.create(user=user_1) - membership.user = user_2 - membership.save() - project_timeline = service.get_project_timeline(membership.project) - user_1_timeline = service.get_user_timeline(user_1) - user_2_timeline = service.get_user_timeline(user_2) - assert project_timeline[0].event_type == "projects.membership.delete" - assert project_timeline[0].data["project"]["id"] == membership.project.id - assert project_timeline[0].data["user"]["id"] == user_1.id - assert project_timeline[1].event_type == "projects.membership.create" - assert project_timeline[1].data["project"]["id"] == membership.project.id - assert project_timeline[1].data["user"]["id"] == user_2.id - assert user_1_timeline[0].event_type == "projects.membership.delete" - assert user_1_timeline[0].data["project"]["id"] == membership.project.id - assert user_1_timeline[0].data["user"]["id"] == user_1.id - assert user_2_timeline[0].event_type == "projects.membership.create" - assert user_2_timeline[0].data["project"]["id"] == membership.project.id - assert user_2_timeline[0].data["user"]["id"] == user_2.id - - def test_delete_project_timeline(): project = factories.ProjectFactory.create(name="test project timeline") user_watcher= factories.UserFactory() @@ -534,9 +511,11 @@ def test_timeline_error_use_member_ids_instead_of_memberships_ids(): history_services.take_snapshot(user_story, user=member_user) user_timeline = service.get_profile_timeline(member_user) - assert len(user_timeline) == 2 + + assert len(user_timeline) == 3 assert user_timeline[0].event_type == "userstories.userstory.create" - assert user_timeline[1].event_type == "users.user.create" + assert user_timeline[1].event_type == "projects.membership.create" + assert user_timeline[2].event_type == "users.user.create" external_user_timeline = service.get_profile_timeline(external_user) assert len(external_user_timeline) == 1 From 513e62fb83f6a3715715500ea37497b5d25e18bf Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 31 May 2016 13:45:09 +0200 Subject: [PATCH 033/261] Fixing permission when project hasn't owner --- taiga/permissions/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/permissions/services.py b/taiga/permissions/services.py index 8cb679a1..32357926 100644 --- a/taiga/permissions/services.py +++ b/taiga/permissions/services.py @@ -51,7 +51,7 @@ def is_project_owner(user, obj): if project is None: return False - if user.id == project.owner.id: + if user.id == project.owner_id: return True return False From eb87f17d2e334298e2066c7933e8b39354825f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 31 May 2016 13:54:29 +0200 Subject: [PATCH 034/261] Fix migrations between master and stable --- .../migrations/0044_auto_20160531_1150.py | 20 +++++++++++++++++++ taiga/projects/migrations/0044_merge.py | 16 --------------- 2 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 taiga/projects/migrations/0044_auto_20160531_1150.py delete mode 100644 taiga/projects/migrations/0044_merge.py diff --git a/taiga/projects/migrations/0044_auto_20160531_1150.py b/taiga/projects/migrations/0044_auto_20160531_1150.py new file mode 100644 index 00000000..67df71a5 --- /dev/null +++ b/taiga/projects/migrations/0044_auto_20160531_1150.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-31 11:50 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0043_auto_20160530_1004'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='blocked_code', + field=models.CharField(blank=True, choices=[('blocked-by-nonpayment', 'This project is blocked due to payment failure'), ('blocked-by-staff', 'This project is blocked by admin staff'), ('blocked-by-owner-leaving', 'This project is blocked because the owner left'), ('blocked-by-deleting', "This project is blocked while it's deleted")], default=None, max_length=255, null=True, verbose_name='blocked code'), + ), + ] diff --git a/taiga/projects/migrations/0044_merge.py b/taiga/projects/migrations/0044_merge.py deleted file mode 100644 index 6bf0227c..00000000 --- a/taiga/projects/migrations/0044_merge.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-05-30 16:36 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('projects', '0043_auto_20160530_1004'), - ('projects', '0042_auto_20160525_0911'), - ] - - operations = [ - ] From c9c66238bdfc7cf4ed10b9b708172a43e86d8ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 31 May 2016 14:00:20 +0200 Subject: [PATCH 035/261] regenerate merge migration --- taiga/projects/migrations/0045_merge.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 taiga/projects/migrations/0045_merge.py diff --git a/taiga/projects/migrations/0045_merge.py b/taiga/projects/migrations/0045_merge.py new file mode 100644 index 00000000..09b3d419 --- /dev/null +++ b/taiga/projects/migrations/0045_merge.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-31 11:59 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0044_auto_20160531_1150'), + ('projects', '0042_auto_20160525_0911'), + ] + + operations = [ + ] From b00ba4a96828206c35d37d734d228fd8e0293814 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 1 Jun 2016 08:16:39 +0200 Subject: [PATCH 036/261] Including created, modified and finished dates in tasks csv reports --- CHANGELOG.md | 1 + taiga/projects/tasks/services.py | 6 +++++- tests/integration/test_tasks.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b090ff..3b1078c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Now comment owners and project admins can edit existing comments with the history Entry endpoint. - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. +- Include created, modified and finished dates for tasks in CSV reports ### Misc - Lots of small and not so small bugfixes. diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 98f9d4a1..427e4f28 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -99,7 +99,8 @@ def tasks_to_csv(project, queryset): fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start", "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order", - "taskboard_order", "attachments", "external_reference", "tags", "watchers", "voters"] + "taskboard_order", "attachments", "external_reference", "tags", "watchers", "voters", + "created_date", "modified_date", "finished_date"] custom_attrs = project.taskcustomattributes.all() for custom_attr in custom_attrs: @@ -141,6 +142,9 @@ def tasks_to_csv(project, queryset): "tags": ",".join(task.tags or []), "watchers": task.watchers, "voters": task.total_voters, + "created_date": task.created_date, + "modified_date": task.modified_date, + "finished_date": task.finished_date, } for custom_attr in custom_attrs: value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 3323e84f..bb077c2f 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -177,6 +177,6 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[21] == attr.name + assert row[24] == attr.name row = next(reader) - assert row[21] == "val1" + assert row[24] == "val1" From f3176f4b5ac31bd8ea970429377d67f3dea9461a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 2 Jun 2016 08:50:06 +0200 Subject: [PATCH 037/261] Detecting errors properly on points --- taiga/projects/userstories/api.py | 30 ++++++++++++++++----------- tests/integration/test_userstories.py | 23 +++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index eb05fe31..c3858555 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from contextlib import suppress - from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ @@ -126,18 +124,26 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi super().pre_save(obj) def post_save(self, obj, created=False): - # Code related to the hack of pre_save method. Rather, - # this is the continuation of it. - - Points = apps.get_model("projects", "Points") - RolePoints = apps.get_model("userstories", "RolePoints") - + # Code related to the hack of pre_save method. Rather, this is the continuation of it. if self._role_points: - with suppress(ObjectDoesNotExist): - for role_id, points_id in self._role_points.items(): - role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk) + Points = apps.get_model("projects", "Points") + RolePoints = apps.get_model("userstories", "RolePoints") + + for role_id, points_id in self._role_points.items(): + try: + role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk, + role__computable=True) + except (ValueError, RolePoints.DoesNotExist): + raise exc.BadRequest({"points": _("Invalid role id '{role_id}'").format( + role_id=role_id)}) + + try: role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id) - role_points.save() + except (ValueError, Points.DoesNotExist): + raise exc.BadRequest({"points": _("Invalid points id '{points_id}'").format( + points_id=points_id)}) + + role_points.save() super().post_save(obj, created) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 842f3089..2a8499a0 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -248,33 +248,42 @@ def test_update_userstory_points(client): f.PointsFactory.create(project=project, value=1) points3 = f.PointsFactory.create(project=project, value=2) - us = f.UserStoryFactory.create(project=project, owner=user1, status__project=project, milestone__project=project) + us = f.UserStoryFactory.create(project=project,owner=user1, status__project=project, + milestone__project=project) usdata = UserStorySerializer(us).data url = reverse("userstories-detail", args=[us.pk]) client.login(user1) - # Api should ignore invalid values + # invalid role data = {} data["version"] = usdata["version"] data["points"] = copy.copy(usdata["points"]) - data["points"].update({'2000': points3.pk}) + data["points"].update({"222222": points3.pk}) response = client.json.patch(url, json.dumps(data)) - assert response.status_code == 200, str(response.content) - assert response.data["points"] == usdata['points'] + assert response.status_code == 400 + + # invalid point + data = {} + data["version"] = usdata["version"] + data["points"] = copy.copy(usdata["points"]) + data["points"].update({str(role1.pk): "999999"}) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 # Api should save successful data = {} - data["version"] = usdata["version"] + 1 + data["version"] = usdata["version"] data["points"] = copy.copy(usdata["points"]) data["points"].update({str(role1.pk): points3.pk}) response = client.json.patch(url, json.dumps(data)) us = models.UserStory.objects.get(pk=us.pk) usdatanew = UserStorySerializer(us).data - assert response.status_code == 200 + assert response.status_code == 200, str(response.content) assert response.data["points"] == usdatanew['points'] assert response.data["points"] != usdata['points'] From d83db761c96610d8aff396b135f53dd4b49d9e0a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 3 Jun 2016 07:37:46 +0200 Subject: [PATCH 038/261] Fixing situations where project owner is none --- taiga/projects/services/members.py | 6 ++++++ taiga/projects/services/projects.py | 13 +++++++++++++ taiga/users/admin.py | 1 - 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py index 4de432ab..8c9bf265 100644 --- a/taiga/projects/services/members.py +++ b/taiga/projects/services/members.py @@ -88,6 +88,9 @@ def get_max_memberships_for_project(project): :return: a number or null. """ + if project.owner is None: + return None + if project.is_private: return project.owner.max_memberships_private_projects return project.owner.max_memberships_public_projects @@ -111,6 +114,9 @@ def check_if_project_can_have_more_memberships(project, total_new_memberships): :return: {bool, error_mesage} return a tuple (can add new members?, error message). """ + if project.owner is None: + return False, _("Project without owner") + if project.is_private: total_memberships = project.memberships.count() + total_new_memberships max_memberships = project.owner.max_memberships_private_projects diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py index 49abc2e5..f56a9941 100644 --- a/taiga/projects/services/projects.py +++ b/taiga/projects/services/projects.py @@ -25,6 +25,7 @@ ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS = 'max_public_projects_memberships' ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS = 'max_private_projects_memberships' ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects' ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects' +ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner' def check_if_project_privacity_can_be_changed(project): """Return if the project privacity can be changed from private to public or viceversa. @@ -33,6 +34,9 @@ def check_if_project_privacity_can_be_changed(project): :return: A dict like this {'can_be_updated': bool, 'reason': error message}. """ + if project.owner is None: + return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} + if project.is_private: current_memberships = project.memberships.count() max_memberships = project.owner.max_memberships_public_projects @@ -66,6 +70,9 @@ def check_if_project_can_be_created_or_updated(project): :return: {bool, error_mesage} return a tuple (can be created or updated, error message). """ + if project.owner is None: + return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} + if project.is_private: current_projects = project.owner.owned_projects.filter(is_private=True).count() max_projects = project.owner.max_private_projects @@ -100,6 +107,9 @@ def check_if_project_can_be_transfered(project, new_owner): :return: {bool, error_mesage} return a tuple (can be transfered?, error message). """ + if project.owner is None: + return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} + if project.owner == new_owner: return (True, None) @@ -136,6 +146,9 @@ def check_if_project_is_out_of_owner_limits(project): :return: bool """ + if project.owner is None: + return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} + if project.is_private: current_memberships = project.memberships.count() max_memberships = project.owner.max_memberships_private_projects diff --git a/taiga/users/admin.py b/taiga/users/admin.py index 1d03d0cf..9c52a9bb 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -64,7 +64,6 @@ class MembershipsInline(admin.TabularInline): def project_owner(self, obj): if obj.project and obj.project.owner: - #return obj.project.owner.get_full_name() return "{} (@{})".format(obj.project.owner.get_full_name(), obj.project.owner.username) return None project_owner.short_description = _("owner") From 0418d1baf97a208a823b938cca174256bc60b151 Mon Sep 17 00:00:00 2001 From: Jubril Issa Date: Sat, 4 Jun 2016 22:02:15 +0100 Subject: [PATCH 039/261] Remove repetition in code --- taiga/projects/issues/serializers.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 6c2f877e..505f8463 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -74,13 +74,6 @@ class IssueListSerializer(IssueSerializer): exclude=("description", "description_html") -class IssueListSerializer(IssueSerializer): - class Meta: - model = models.Issue - read_only_fields = ('id', 'ref', 'created_date', 'modified_date') - exclude=("description", "description_html") - - class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer): def serialize_neighbor(self, neighbor): if neighbor: From 3fd332219cd2504a472b6244c086dc4694d9daf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 Jun 2016 12:01:23 +0200 Subject: [PATCH 040/261] Fix race condition on projects refresh totals --- taiga/projects/models.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 060fbdb8..4a369c8d 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -339,7 +339,17 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): self.total_activity_last_year = qs_year.count() if save: - self.save() + self.save(update_fields=[ + 'totals_updated_datetime', + 'total_fans', + 'total_fans_last_week', + 'total_fans_last_month', + 'total_fans_last_year', + 'total_activity', + 'total_activity_last_week', + 'total_activity_last_month', + 'total_activity_last_year', + ]) @cached_property def cached_user_stories(self): From ccc6491c8343870ffd5d8949fa0d1015dcf1f780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 Jun 2016 13:11:25 +0200 Subject: [PATCH 041/261] Enhancement#3758: Include gravatar url into users serializer --- CHANGELOG.md | 1 + taiga/users/serializers.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1078c3..d4a82a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Now comment owners and project admins can edit existing comments with the history Entry endpoint. - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. - Include created, modified and finished dates for tasks in CSV reports +- Add gravatar url to Users API endpoint. ### Misc - Lots of small and not so small bugfixes. diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 97aeafca..e35e56cb 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -22,12 +22,13 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers -from taiga.base.fields import PgArrayField, TagsField +from taiga.base.fields import PgArrayField from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project from .models import User, Role from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url +from .gravatar import get_gravatar_url from collections import namedtuple @@ -48,6 +49,7 @@ class UserSerializer(serializers.ModelSerializer): full_name_display = serializers.SerializerMethodField("get_full_name_display") photo = serializers.SerializerMethodField("get_photo") big_photo = serializers.SerializerMethodField("get_big_photo") + gravatar_url = serializers.SerializerMethodField("get_gravatar_url") roles = serializers.SerializerMethodField("get_roles") projects_with_me = serializers.SerializerMethodField("get_projects_with_me") @@ -57,7 +59,8 @@ class UserSerializer(serializers.ModelSerializer): # with this info (including there the email) fields = ("id", "username", "full_name", "full_name_display", "color", "bio", "lang", "theme", "timezone", "is_active", - "photo", "big_photo", "roles", "projects_with_me") + "photo", "big_photo", "roles", "projects_with_me", + "gravatar_url") read_only_fields = ("id",) def validate_username(self, attrs, source): @@ -87,6 +90,9 @@ class UserSerializer(serializers.ModelSerializer): def get_big_photo(self, user): return get_big_photo_or_gravatar_url(user) + def get_gravatar_url(self, user): + return get_gravatar_url(user.email) + def get_roles(self, user): return user.memberships. order_by("role__name").values_list("role__name", flat=True).distinct() @@ -104,6 +110,7 @@ class UserSerializer(serializers.ModelSerializer): projects = Project.objects.filter(id__in=project_ids) return ContactProjectDetailSerializer(projects, many=True).data + class UserAdminSerializer(UserSerializer): total_private_projects = serializers.SerializerMethodField("get_total_private_projects") total_public_projects = serializers.SerializerMethodField("get_total_public_projects") @@ -114,7 +121,7 @@ class UserAdminSerializer(UserSerializer): # with this info (including here the email) fields = ("id", "username", "full_name", "full_name_display", "email", "color", "bio", "lang", "theme", "timezone", "is_active", "photo", - "big_photo", + "big_photo", "gravatar_url", "max_private_projects", "max_public_projects", "max_memberships_private_projects", "max_memberships_public_projects", "total_private_projects", "total_public_projects") @@ -134,7 +141,7 @@ class UserAdminSerializer(UserSerializer): class UserBasicInfoSerializer(UserSerializer): class Meta: model = User - fields = ("username", "full_name_display","photo", "big_photo", "is_active", "id") + fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id") class RecoverySerializer(serializers.Serializer): @@ -178,7 +185,6 @@ class ProjectRoleSerializer(serializers.ModelSerializer): ## Like ###################################################### - class HighLightedContentSerializer(serializers.Serializer): type = serializers.CharField() id = serializers.IntegerField() @@ -279,7 +285,7 @@ class LikedObjectSerializer(HighLightedContentSerializer): def __init__(self, *args, **kwargs): # Don't pass the extra ids args up to the superclass - self.user_likes = kwargs.pop("user_likes", {}) + self.user_likes = kwargs.pop("user_likes", {}) # Instantiate the superclass normally super().__init__(*args, **kwargs) @@ -294,7 +300,7 @@ class VotedObjectSerializer(HighLightedContentSerializer): def __init__(self, *args, **kwargs): # Don't pass the extra ids args up to the superclass - self.user_votes = kwargs.pop("user_votes", {}) + self.user_votes = kwargs.pop("user_votes", {}) # Instantiate the superclass normally super().__init__(*args, **kwargs) From edced6ed0ed006809fa1491e24c77cb025f25772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 Jun 2016 09:33:21 +0200 Subject: [PATCH 042/261] Marking as "html" safe email subjects --- .../export_import/templates/emails/dump_project-subject.jinja | 2 +- .../export_import/templates/emails/export_error-subject.jinja | 2 +- .../export_import/templates/emails/import_error-subject.jinja | 2 +- taiga/export_import/templates/emails/load_dump-subject.jinja | 2 +- .../templates/emails/feedback_notification-subject.jinja | 2 +- .../templates/emails/issues/issue-change-subject.jinja | 2 +- .../templates/emails/issues/issue-create-subject.jinja | 2 +- .../templates/emails/issues/issue-delete-subject.jinja | 2 +- .../emails/milestones/milestone-change-subject.jinja | 2 +- .../emails/milestones/milestone-create-subject.jinja | 2 +- .../emails/milestones/milestone-delete-subject.jinja | 2 +- .../templates/emails/tasks/task-change-subject.jinja | 4 ++-- .../templates/emails/tasks/task-create-subject.jinja | 2 +- .../templates/emails/tasks/task-delete-subject.jinja | 2 +- .../emails/userstories/userstory-change-subject.jinja | 2 +- .../emails/userstories/userstory-create-subject.jinja | 2 +- .../emails/userstories/userstory-delete-subject.jinja | 2 +- .../templates/emails/wiki/wikipage-change-subject.jinja | 2 +- .../templates/emails/wiki/wikipage-create-subject.jinja | 2 +- .../templates/emails/wiki/wikipage-delete-subject.jinja | 2 +- .../templates/emails/membership_invitation-subject.jinja | 2 +- .../templates/emails/membership_notification-subject.jinja | 2 +- taiga/projects/templates/emails/transfer_accept-subject.jinja | 2 +- taiga/projects/templates/emails/transfer_reject-subject.jinja | 2 +- .../projects/templates/emails/transfer_request-subject.jinja | 2 +- taiga/projects/templates/emails/transfer_start-subject.jinja | 2 +- 26 files changed, 27 insertions(+), 27 deletions(-) diff --git a/taiga/export_import/templates/emails/dump_project-subject.jinja b/taiga/export_import/templates/emails/dump_project-subject.jinja index f723a922..7ad0ef61 100644 --- a/taiga/export_import/templates/emails/dump_project-subject.jinja +++ b/taiga/export_import/templates/emails/dump_project-subject.jinja @@ -1 +1 @@ -{% trans project=project.name %}[{{ project }}] Your project dump has been generated{% endtrans %} +{% trans project=project.name|safe %}[{{ project }}] Your project dump has been generated{% endtrans %} diff --git a/taiga/export_import/templates/emails/export_error-subject.jinja b/taiga/export_import/templates/emails/export_error-subject.jinja index e5d020cf..741bf6d8 100644 --- a/taiga/export_import/templates/emails/export_error-subject.jinja +++ b/taiga/export_import/templates/emails/export_error-subject.jinja @@ -1 +1 @@ -{% trans error_subject=error_subject, project=project.name %}[{{ project }}] {{ error_subject }}{% endtrans %} +{% trans error_subject=error_subject|safe, project=project.name|safe %}[{{ project }}] {{ error_subject }}{% endtrans %} diff --git a/taiga/export_import/templates/emails/import_error-subject.jinja b/taiga/export_import/templates/emails/import_error-subject.jinja index 44506406..329983d4 100644 --- a/taiga/export_import/templates/emails/import_error-subject.jinja +++ b/taiga/export_import/templates/emails/import_error-subject.jinja @@ -1 +1 @@ -{% trans error_subject=error_subject %}[Taiga] {{ error_subject }}{% endtrans %} +{% trans error_subject=error_subject|safe %}[Taiga] {{ error_subject }}{% endtrans %} diff --git a/taiga/export_import/templates/emails/load_dump-subject.jinja b/taiga/export_import/templates/emails/load_dump-subject.jinja index 6ef621c4..a258d42e 100644 --- a/taiga/export_import/templates/emails/load_dump-subject.jinja +++ b/taiga/export_import/templates/emails/load_dump-subject.jinja @@ -1 +1 @@ -{% trans project=project.name %}[{{ project }}] Your project dump has been imported{% endtrans %} +{% trans project=project.name|safe %}[{{ project }}] Your project dump has been imported{% endtrans %} diff --git a/taiga/feedback/templates/emails/feedback_notification-subject.jinja b/taiga/feedback/templates/emails/feedback_notification-subject.jinja index a5fe9c3f..6cc87701 100644 --- a/taiga/feedback/templates/emails/feedback_notification-subject.jinja +++ b/taiga/feedback/templates/emails/feedback_notification-subject.jinja @@ -1,3 +1,3 @@ -{% trans full_name=feedback_entry.full_name, email=feedback_entry.email %} +{% trans full_name=feedback_entry.full_name|safe, email=feedback_entry.email|safe %} [Taiga] Feedback from {{ full_name }} <{{ email }}> {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja index 5cee74b7..707e2703 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Updated the issue #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja index cde770ce..7e4cf6bd 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Created the issue #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja b/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja index c5773908..bf297fa4 100644 --- a/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Deleted the issue #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja index 1891a893..400bf944 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, milestone=snapshot.name %} +{% trans project=project.name|safe, milestone=snapshot.name|safe %} [{{ project }}] Updated the sprint "{{ milestone }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja index 700faac0..10656b83 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, milestone=snapshot.name %} +{% trans project=project.name|safe, milestone=snapshot.name|safe %} [{{ project }}] Created the sprint "{{ milestone }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja index 242f1326..11404786 100644 --- a/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, milestone=snapshot.name %} +{% trans project=project.name|safe, milestone=snapshot.name|safe %} [{{ project }}] Deleted the Sprint "{{ milestone }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja index 6275e839..44fde7e5 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} -[{{ project }}] Updated the task #{{ ref }} "{{ subject }}" +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +[{{ project }}] Updated the task #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja index 27dabde4..d107e855 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Created the task #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja b/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja index 8522f00c..e80e164f 100644 --- a/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Deleted the task #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja index 3ed70685..e116e403 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Updated the US #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja index 1e166e57..124e3060 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Created the US #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja index 3a48058e..f7843217 100644 --- a/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} [{{ project }}] Deleted the US #{{ ref }} "{{ subject }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja index 73912cb6..14bab7d5 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-change-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, page=snapshot.slug %} +{% trans project=project.name|safe, page=snapshot.slug|safe %} [{{ project }}] Updated the Wiki Page "{{ page }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja index 6b7d1adf..92560812 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-create-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, page=snapshot.slug %} +{% trans project=project.name|safe, page=snapshot.slug|safe %} [{{ project }}] Created the Wiki Page "{{ page }}" {% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja index a807f6f3..244410fc 100644 --- a/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja +++ b/taiga/projects/notifications/templates/emails/wiki/wikipage-delete-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name, page=snapshot.slug %} +{% trans project=project.name|safe, page=snapshot.slug|safe %} [{{ project }}] Deleted the Wiki Page "{{ page }}" {% endtrans %} diff --git a/taiga/projects/templates/emails/membership_invitation-subject.jinja b/taiga/projects/templates/emails/membership_invitation-subject.jinja index 8e620317..0b5206ef 100644 --- a/taiga/projects/templates/emails/membership_invitation-subject.jinja +++ b/taiga/projects/templates/emails/membership_invitation-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=membership.project %} +{% trans project=membership.project|safe %} [Taiga] Invitation to join to the project '{{ project }}' {% endtrans %} diff --git a/taiga/projects/templates/emails/membership_notification-subject.jinja b/taiga/projects/templates/emails/membership_notification-subject.jinja index c6bdd588..57d60ac6 100644 --- a/taiga/projects/templates/emails/membership_notification-subject.jinja +++ b/taiga/projects/templates/emails/membership_notification-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=membership.project %} +{% trans project=membership.project|safe %} [Taiga] Added to the project '{{ project }}' {% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_accept-subject.jinja b/taiga/projects/templates/emails/transfer_accept-subject.jinja index 6b7c84d5..75380521 100644 --- a/taiga/projects/templates/emails/transfer_accept-subject.jinja +++ b/taiga/projects/templates/emails/transfer_accept-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name %} +{% trans project=project.name|safe %} [{{project}}] Project ownership transfer offer accepted! {% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_reject-subject.jinja b/taiga/projects/templates/emails/transfer_reject-subject.jinja index e6eaa127..345ae940 100644 --- a/taiga/projects/templates/emails/transfer_reject-subject.jinja +++ b/taiga/projects/templates/emails/transfer_reject-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name %} +{% trans project=project.name|safe %} [{{project}}] Project ownership transfer declined {% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_request-subject.jinja b/taiga/projects/templates/emails/transfer_request-subject.jinja index 1f6ff81c..ea4efbbb 100644 --- a/taiga/projects/templates/emails/transfer_request-subject.jinja +++ b/taiga/projects/templates/emails/transfer_request-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name %} +{% trans project=project.name|safe %} [{{project}}] Project ownership transfer request {% endtrans %} diff --git a/taiga/projects/templates/emails/transfer_start-subject.jinja b/taiga/projects/templates/emails/transfer_start-subject.jinja index d0e34d3a..c2791053 100644 --- a/taiga/projects/templates/emails/transfer_start-subject.jinja +++ b/taiga/projects/templates/emails/transfer_start-subject.jinja @@ -1,3 +1,3 @@ -{% trans project=project.name %} +{% trans project=project.name|safe %} [{{project}}] Project ownership transfer offer {% endtrans %} From 3080c0e5f27613a8c9476ae7300c4f6b86d6c2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 15 Jun 2016 09:21:41 +0200 Subject: [PATCH 043/261] Change default order for wiki links --- .../management/commands/sample_data.py | 35 ++++++++++++++----- .../migrations/0003_auto_20160615_0721.py | 24 +++++++++++++ taiga/projects/wiki/models.py | 4 +-- 3 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 taiga/projects/wiki/migrations/0003_auto_20160615_0721.py diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 0c355c2f..db7a696c 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -113,6 +113,7 @@ NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5)) NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4)) NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20)) NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25)) +NUM_WIKI_LINKS = getattr(settings, "SAMPLE_DATA_NUM_WIKI_LINKS", (0, 15)) NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4)) NUM_LIKES = getattr(settings, "SAMPLE_DATA_NUM_LIKES", (0, 10)) NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 10)) @@ -186,22 +187,25 @@ class Command(BaseCommand): # added custom attributes if self.sd.boolean: - for i in range(1, 4): - UserStoryCustomAttribute.objects.create(name=self.sd.words(1, 3), + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + UserStoryCustomAttribute.objects.create(name=name, description=self.sd.words(3, 12), type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) if self.sd.boolean: - for i in range(1, 4): - TaskCustomAttribute.objects.create(name=self.sd.words(1, 3), + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + TaskCustomAttribute.objects.create(name=name, description=self.sd.words(3, 12), type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) if self.sd.boolean: - for i in range(1, 4): - IssueCustomAttribute.objects.create(name=self.sd.words(1, 3), + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + IssueCustomAttribute.objects.create(name=name, description=self.sd.words(3, 12), type=self.sd.choice(TYPES_CHOICES)[0], project=project, @@ -243,8 +247,14 @@ class Command(BaseCommand): for y in range(self.sd.int(*NUM_ISSUES)): bug = self.create_bug(project) - # create a wiki page - wiki_page = self.create_wiki(project, "home") + # create a wiki pages and wiki links + wiki_page = self.create_wiki_page(project, "home") + + for y in range(self.sd.int(*NUM_WIKI_LINKS)): + wiki_link = self.create_wiki_link(project) + if self.sd.boolean(): + self.create_wiki_page(project, wiki_link.href) + # Set a value to total_story_points to show the deadline in the backlog project_stats = get_stats_for_project(project) @@ -270,7 +280,14 @@ class Command(BaseCommand): attached_file=attached_file) return attachment - def create_wiki(self, project, slug): + + def create_wiki_link(self, project, title=None): + wiki_link = WikiLink.objects.create(project=project, + title=title or self.sd.words(1, 3)) + return wiki_link + + + def create_wiki_page(self, project, slug): wiki_page = WikiPage.objects.create(project=project, slug=slug, content=self.sd.paragraphs(3,15), diff --git a/taiga/projects/wiki/migrations/0003_auto_20160615_0721.py b/taiga/projects/wiki/migrations/0003_auto_20160615_0721.py new file mode 100644 index 00000000..1e1876e0 --- /dev/null +++ b/taiga/projects/wiki/migrations/0003_auto_20160615_0721.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-15 07:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wiki', '0002_remove_wikipage_watchers'), + ] + + operations = [ + migrations.AlterModelOptions( + name='wikilink', + options={'ordering': ['project', 'order', 'id'], 'verbose_name': 'wiki link', 'verbose_name_plural': 'wiki links'}, + ), + migrations.AlterField( + model_name='wikilink', + name='order', + field=models.PositiveSmallIntegerField(default='10000', verbose_name='order'), + ), + ] diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index 659e51f0..19cd6b25 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -70,13 +70,13 @@ class WikiLink(models.Model): title = models.CharField(max_length=500, null=False, blank=False) href = models.SlugField(max_length=500, db_index=True, null=False, blank=False, verbose_name=_("href")) - order = models.PositiveSmallIntegerField(default=1, null=False, blank=False, + order = models.PositiveSmallIntegerField(null=False, blank=False, default="10000", verbose_name=_("order")) class Meta: verbose_name = "wiki link" verbose_name_plural = "wiki links" - ordering = ["project", "order"] + ordering = ["project", "order", "id"] unique_together = ("project", "href") def __str__(self): From 20c350ea63b987e7f82207f4f80c919c05748389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 6 Jun 2016 15:11:48 +0200 Subject: [PATCH 044/261] Remove obsolete code --- taiga/projects/apps.py | 5 --- taiga/projects/issues/apps.py | 17 +++---- taiga/projects/issues/models.py | 2 - taiga/projects/services/__init__.py | 2 - taiga/projects/services/tags_colors.py | 62 -------------------------- taiga/projects/signals.py | 9 +--- taiga/projects/tasks/apps.py | 26 +++++------ taiga/projects/userstories/apps.py | 33 +++++++------- 8 files changed, 38 insertions(+), 118 deletions(-) delete mode 100644 taiga/projects/services/tags_colors.py diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py index a390b5f5..38295e81 100644 --- a/taiga/projects/apps.py +++ b/taiga/projects/apps.py @@ -34,9 +34,6 @@ def connect_projects_signals(): signals.pre_save.connect(handlers.tags_normalization, sender=apps.get_model("projects", "Project"), dispatch_uid="tags_normalization_projects") - signals.pre_save.connect(handlers.update_project_tags_when_create_or_edit_taggable_item, - sender=apps.get_model("projects", "Project"), - dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects") def disconnect_projects_signals(): @@ -44,8 +41,6 @@ def disconnect_projects_signals(): dispatch_uid='project_post_save') signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"), dispatch_uid="tags_normalization_projects") - signals.pre_save.disconnect(sender=apps.get_model("projects", "Project"), - dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_projects") ## Memberships Signals diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index 4d0bca19..671a45be 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -34,12 +34,6 @@ def connect_issues_signals(): signals.pre_save.connect(generic_handlers.tags_normalization, sender=apps.get_model("issues", "Issue"), dispatch_uid="tags_normalization_issue") - signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, - sender=apps.get_model("issues", "Issue"), - dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue") - signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, - sender=apps.get_model("issues", "Issue"), - dispatch_uid="update_project_tags_when_delete_taggable_item_issue") def connect_issues_custom_attributes_signals(): @@ -56,14 +50,15 @@ def connect_all_issues_signals(): def disconnect_issues_signals(): - signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="set_finished_date_when_edit_issue") - signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="tags_normalization_issue") - signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_issue") - signals.post_delete.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="update_project_tags_when_delete_taggable_item_issue") + signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), + dispatch_uid="set_finished_date_when_edit_issue") + signals.pre_save.disconnect(sender=apps.get_model("issues", "Issue"), + dispatch_uid="tags_normalization_issue") def disconnect_issues_custom_attributes_signals(): - signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), dispatch_uid="create_custom_attribute_value_when_create_issue") + signals.post_save.disconnect(sender=apps.get_model("issues", "Issue"), + dispatch_uid="create_custom_attribute_value_when_create_issue") def disconnect_all_issues_signals(): diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 89a78051..cd962e08 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -30,8 +30,6 @@ from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin from taiga.base.tags import TaggedMixin -from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags - class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index fb3cb9c5..a115275b 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -55,7 +55,5 @@ from .stats import get_stats_for_project_issues from .stats import get_stats_for_project from .stats import get_member_stats_for_project -from .tags_colors import update_project_tags_colors_handler - from .transfer import request_project_transfer, start_project_transfer from .transfer import accept_project_transfer, reject_project_transfer diff --git a/taiga/projects/services/tags_colors.py b/taiga/projects/services/tags_colors.py deleted file mode 100644 index 9b9aa962..00000000 --- a/taiga/projects/services/tags_colors.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# 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.conf import settings - -from taiga.projects.services.filters import get_all_tags -from taiga.projects.models import Project - -from hashlib import sha1 - - -def _generate_color(tag): - color = sha1(tag.encode("utf-8")).hexdigest()[0:6] - return "#{}".format(color) - - -def _get_new_color(tag, predefined_colors, exclude=[]): - colors = list(set(predefined_colors) - set(exclude)) - if colors: - return colors[0] - return _generate_color(tag) - - -def remove_unused_tags(project): - current_tags = get_all_tags(project) - project.tags_colors = list(filter(lambda x: x[0] in current_tags, project.tags_colors)) - - -def update_project_tags_colors_handler(instance): - if instance.tags is None: - instance.tags = [] - - if not isinstance(instance.project.tags_colors, list): - instance.project.tags_colors = [] - - for tag in instance.tags: - defined_tags = map(lambda x: x[0], instance.project.tags_colors) - if tag not in defined_tags: - used_colors = map(lambda x: x[1], instance.project.tags_colors) - new_color = _get_new_color(tag, settings.TAGS_PREDEFINED_COLORS, - exclude=used_colors) - instance.project.tags_colors.append([tag, new_color]) - - remove_unused_tags(instance.project) - - if not isinstance(instance, Project): - instance.project.save() diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py index ca5d7094..e0196887 100644 --- a/taiga/projects/signals.py +++ b/taiga/projects/signals.py @@ -19,7 +19,6 @@ from django.apps import apps from django.conf import settings -from taiga.projects.services.tags_colors import update_project_tags_colors_handler, remove_unused_tags from taiga.projects.notifications.services import create_notify_policy_if_not_exists from taiga.base.utils.db import get_typename_for_model_class @@ -37,13 +36,7 @@ def tags_normalization(sender, instance, **kwargs): instance.tags = list(map(str.lower, instance.tags)) -def update_project_tags_when_create_or_edit_taggable_item(sender, instance, **kwargs): - update_project_tags_colors_handler(instance) - - -def update_project_tags_when_delete_taggable_item(sender, instance, **kwargs): - remove_unused_tags(instance.project) - instance.project.save() +## Membership def membership_post_delete(sender, instance, using, **kwargs): instance.project.update_role_points() diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 616854f6..23cfdfb0 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -32,12 +32,7 @@ def connect_tasks_signals(): signals.pre_save.connect(generic_handlers.tags_normalization, sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization_task") - signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, - sender=apps.get_model("tasks", "Task"), - dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item_task") - signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, - sender=apps.get_model("tasks", "Task"), - dispatch_uid="update_project_tags_when_delete_tagglabe_item_task") + def connect_tasks_close_or_open_us_and_milestone_signals(): from . import signals as handlers @@ -67,19 +62,24 @@ def connect_all_tasks_signals(): def disconnect_tasks_signals(): - signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization") - signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_create_or_edit_tagglabe_item") - signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="update_project_tags_when_delete_tagglabe_item") + signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), + dispatch_uid="set_finished_date_when_edit_task") + signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), + dispatch_uid="tags_normalization") def disconnect_tasks_close_or_open_us_and_milestone_signals(): - signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="cached_prev_task") - signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task") - signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") + signals.pre_save.disconnect(sender=apps.get_model("tasks", "Task"), + dispatch_uid="cached_prev_task") + signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), + dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_task") + signals.post_delete.disconnect(sender=apps.get_model("tasks", "Task"), + dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") def disconnect_tasks_custom_attributes_signals(): - signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), dispatch_uid="create_custom_attribute_value_when_create_task") + signals.post_save.disconnect(sender=apps.get_model("tasks", "Task"), + dispatch_uid="create_custom_attribute_value_when_create_task") def disconnect_all_tasks_signals(): diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index ef3d5df5..04c7d32d 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -62,12 +62,6 @@ def connect_userstories_signals(): signals.pre_save.connect(generic_handlers.tags_normalization, sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story") - signals.post_save.connect(generic_handlers.update_project_tags_when_create_or_edit_taggable_item, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") - signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") def connect_userstories_custom_attributes_signals(): @@ -83,18 +77,27 @@ def connect_all_userstories_signals(): def disconnect_userstories_signals(): - signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="cached_prev_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_role_points_when_create_or_edit_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_milestone_of_tasks_when_edit_us") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") - signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="try_to_close_milestone_when_delete_us") - signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story") - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_create_or_edit_taggable_item_user_story") - signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="update_project_tags_when_delete_taggable_item_user_story") + signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="cached_prev_us") + + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="update_role_points_when_create_or_edit_us") + + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="update_milestone_of_tasks_when_edit_us") + + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="try_to_close_or_open_us_and_milestone_when_create_or_edit_us") + signals.post_delete.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="try_to_close_milestone_when_delete_us") + + signals.pre_save.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="tags_normalization_user_story") def disconnect_userstories_custom_attributes_signals(): - signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), dispatch_uid="create_custom_attribute_value_when_create_user_story") + signals.post_save.disconnect(sender=apps.get_model("userstories", "UserStory"), + dispatch_uid="create_custom_attribute_value_when_create_user_story") def disconnect_all_userstories_signals(): From 7134d04262512a65b59493ff2a009893fddc917d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 7 Jun 2016 09:13:38 +0200 Subject: [PATCH 045/261] Adding migrations --- .../0046_triggers_to_update_tags_colors.py | 192 ++++++++++++++++++ tests/integration/test_projects.py | 24 +++ 2 files changed, 216 insertions(+) create mode 100644 taiga/projects/migrations/0046_triggers_to_update_tags_colors.py diff --git a/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py new file mode 100644 index 00000000..28296036 --- /dev/null +++ b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-07 06:19 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0045_merge'), + ] + + operations = [ + # Function: Reduce a multidimensional array only on its first level + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION public.reduce_dim(anyarray) + RETURNS SETOF anyarray + AS $function$ + DECLARE + s $1%TYPE; + BEGIN + FOREACH s SLICE 1 IN ARRAY $1 LOOP + RETURN NEXT s; + END LOOP; + RETURN; + END; + $function$ + LANGUAGE plpgsql IMMUTABLE; + """ + ), + # Function: aggregates multi dimensional arrays + migrations.RunSQL( + """ + DROP AGGREGATE IF EXISTS array_agg_mult (anyarray); + CREATE AGGREGATE array_agg_mult (anyarray) ( + SFUNC = array_cat + ,STYPE = anyarray + ,INITCOND = '{}' + ); + """ + ), + # Function: array_distinct + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION array_distinct(anyarray) + RETURNS anyarray AS $$ + SELECT ARRAY(SELECT DISTINCT unnest($1)) + $$ LANGUAGE sql; + """ + ), + # Rebuild the color tags so it's consisten in any project + migrations.RunSQL( + """ + WITH + tags_colors AS ( + SELECT id project_id, reduce_dim(tags_colors) tags_colors + FROM projects_project + WHERE tags_colors != '{}' + ), + tags AS ( + SELECT unnest(tags) tag, NULL color, project_id FROM userstories_userstory + UNION + SELECT unnest(tags) tag, NULL color, project_id FROM tasks_task + UNION + SELECT unnest(tags) tag, NULL color, project_id FROM issues_issue + UNION + SELECT unnest(tags) tag, NULL color, id project_id FROM projects_project + ), + rebuilt_tags_colors AS ( + SELECT tags.project_id project_id, + array_agg_mult(ARRAY[[tags.tag, tags_colors.tags_colors[2]]]) tags_colors + FROM tags + LEFT JOIN tags_colors ON + tags_colors.project_id = tags.project_id AND + tags_colors[1] = tags.tag + GROUP BY tags.project_id + ) + UPDATE projects_project + SET tags_colors = rebuilt_tags_colors.tags_colors + FROM rebuilt_tags_colors + WHERE rebuilt_tags_colors.project_id = projects_project.id; + """ + ), + # Trigger for auto updating projects_project.tags_colors + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION update_project_tags_colors() + RETURNS trigger AS $update_project_tags_colors$ + DECLARE + tags text[]; + project_tags_colors text[]; + tag_color text[]; + project_tags text[]; + tag text; + project_id integer; + BEGIN + tags := NEW.tags::text[]; + project_id := NEW.project_id::integer; + project_tags := '{}'; + + -- Read project tags_colors into project_tags_colors + SELECT projects_project.tags_colors INTO project_tags_colors + FROM projects_project + WHERE id = project_id; + + -- Extract just the project tags to project_tags_colors + IF project_tags_colors != ARRAY[]::text[] THEN + FOREACH tag_color SLICE 1 in ARRAY project_tags_colors + LOOP + project_tags := array_append(project_tags, tag_color[1]); + END LOOP; + END IF; + + -- Add to project_tags_colors the new tags + IF tags IS NOT NULL THEN + FOREACH tag in ARRAY tags + LOOP + IF tag != ALL(project_tags) THEN + project_tags_colors := array_cat(project_tags_colors, + ARRAY[ARRAY[tag, NULL]]); + END IF; + END LOOP; + END IF; + + -- Save the result in the tags_colors column + UPDATE projects_project + SET tags_colors = project_tags_colors + WHERE id = project_id; + + RETURN NULL; + END; $update_project_tags_colors$ + LANGUAGE plpgsql; + """ + ), + + # Execute trigger after user_story update + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_userstory_update ON userstories_userstory; + CREATE TRIGGER update_project_tags_colors_on_userstory_update + AFTER UPDATE ON userstories_userstory + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + # Execute trigger after user_story insert + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_userstory_insert ON userstories_userstory; + CREATE TRIGGER update_project_tags_colors_on_userstory_insert + AFTER INSERT ON userstories_userstory + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + # Execute trigger after task update + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_task_update ON tasks_task; + CREATE TRIGGER update_project_tags_colors_on_task_update + AFTER UPDATE ON tasks_task + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + # Execute trigger after task insert + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_task_insert ON tasks_task; + CREATE TRIGGER update_project_tags_colors_on_task_insert + AFTER INSERT ON tasks_task + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + # Execute trigger after issue update + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_issue_update ON issues_issue; + CREATE TRIGGER update_project_tags_colors_on_issue_update + AFTER UPDATE ON issues_issue + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + # Execute trigger after issue insert + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_issue_insert ON issues_issue; + CREATE TRIGGER update_project_tags_colors_on_issue_insert + AFTER INSERT ON issues_issue + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + ] diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 9f91cd21..4c2e121e 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1852,3 +1852,27 @@ def test_delete_project_with_celery_disabled(client, settings): response = client.json.delete(url) assert response.status_code == 204 assert Project.objects.filter(id=project.id).count() == 0 + + +def test_color_tags_project_fired_on_element_create(): + user_story = f.UserStoryFactory.create(tags=["tag"]) + project = Project.objects.get(id=user_story.project.id) + assert project.tags_colors == [["tag", None]] + + +def test_color_tags_project_fired_on_element_update(): + user_story = f.UserStoryFactory.create() + user_story.tags = ["tag"] + user_story.save() + project = Project.objects.get(id=user_story.project.id) + assert project.tags_colors == [["tag", None]] + + +def test_color_tags_project_fired_on_element_update_respecting_color(): + project = f.ProjectFactory.create(tags_colors=[["tag", "#123123"]]) + user_story = f.UserStoryFactory.create(project=project) + user_story.tags = ["tag"] + user_story.save() + project = Project.objects.get(id=user_story.project.id) + assert project.tags_colors == [["tag", "#123123"]] +>>>>>>> d64d158... WIP: migrations, removing automatic color generation From 3e555de7c4d1d1c511ef7d87da76ac557030e84d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 7 Jun 2016 13:26:28 +0200 Subject: [PATCH 046/261] API calls to create, rename, edit color, delete and mix tags --- taiga/projects/api.py | 61 ++++++ taiga/projects/permissions.py | 4 + taiga/projects/serializers.py | 94 ++++++++- taiga/projects/services/__init__.py | 3 + taiga/projects/services/tags.py | 90 +++++++++ .../test_projects_choices_resources.py | 136 ++++++++++++- tests/integration/test_projects.py | 187 +++++++++++++++++- tests/integration/test_users.py | 14 +- 8 files changed, 571 insertions(+), 18 deletions(-) create mode 100644 taiga/projects/services/tags.py diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 9c00901c..958ed3b3 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -405,6 +405,67 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, services.reject_project_transfer(project, request.user, token, reason) return response.Ok() + @detail_route(methods=["POST"]) + def create_tag(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "create_tag", project) + self._raise_if_blocked(project) + serializer = serializers.CreateTagSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.create_tag(project, data.get("tag"), data.get("color")) + return response.Ok() + + + @detail_route(methods=["POST"]) + def edit_tag(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "edit_tag", project) + self._raise_if_blocked(project) + serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.edit_tag(project, data.get("from_tag"), + to_tag=data.get("to_tag", None), + color=data.get("color", None)) + + return response.Ok() + + + @detail_route(methods=["POST"]) + def delete_tag(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "delete_tag", project) + self._raise_if_blocked(project) + serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.delete_tag(project, data.get("tag")) + return response.Ok() + + @detail_route(methods=["POST"]) + def mix_tags(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "mix_tags", project) + self._raise_if_blocked(project) + serializer = serializers.MixTagsSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.mix_tags(project, data.get("from_tags"), data.get("to_tag")) + return response.Ok() + + def _raise_if_blocked(self, project): + if self.is_blocked(project): + raise exc.Blocked(_("Blocked element")) + def _set_base_permissions(self, obj): update_permissions = False if not obj.id: diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index c43e842f..0cc95427 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -78,6 +78,10 @@ class ProjectPermission(TaigaResourcePermission): transfer_start_perms = IsObjectOwner() transfer_reject_perms = IsAuthenticated() & HasProjectPerm('view_project') transfer_accept_perms = IsAuthenticated() & HasProjectPerm('view_project') + create_tag_perms = IsProjectAdmin() + edit_tag_perms = IsProjectAdmin() + delete_tag_perms = IsProjectAdmin() + mix_tags_perms = IsProjectAdmin() class ProjectFansPermission(TaigaResourcePermission): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 1b271590..f7403389 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import re from django.utils.translation import ugettext as _ from django.db.models import Q @@ -256,7 +257,7 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ i_am_member = serializers.SerializerMethodField("get_i_am_member") tags = TagsField(default=[], required=False) - tags_colors = TagsColorsField(required=False) + tags_colors = TagsColorsField(required=False, read_only=True) notify_level = serializers.SerializerMethodField("get_notify_level") total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones") @@ -416,3 +417,94 @@ class ProjectTemplateSerializer(serializers.ModelSerializer): class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer): project_id = serializers.IntegerField() order = serializers.IntegerField() + + +###################################################### +## Project tags serializers +###################################################### + + +class ProjectTagSerializer(serializers.Serializer): + def __init__(self, *args, **kwargs): + # Don't pass the extra project arg + self.project = kwargs.pop("project") + + # Instantiate the superclass normally + super().__init__(*args, **kwargs) + + +class CreateTagSerializer(ProjectTagSerializer): + tag = serializers.CharField() + color = serializers.CharField(required=False) + + def validate_tag(self, attrs, source): + tag = attrs.get(source, None) + if services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag exists.")) + + return attrs + + def validate_color(self, attrs, source): + color = attrs.get(source, None) + if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + raise serializers.ValidationError(_("The color is not a valid HEX color.")) + + return attrs + + +class EditTagTagSerializer(ProjectTagSerializer): + from_tag = serializers.CharField() + to_tag = serializers.CharField(required=False) + color = serializers.CharField(required=False) + + def validate_from_tag(self, attrs, source): + tag = attrs.get(source, None) + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs + + def validate_to_tag(self, attrs, source): + tag = attrs.get(source, None) + if services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag exists yet")) + + return attrs + + def validate_color(self, attrs, source): + color = attrs.get(source, None) + if len(color) != 7 or not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + raise serializers.ValidationError(_("The color is not a valid HEX color.")) + + return attrs + + +class DeleteTagSerializer(ProjectTagSerializer): + tag = serializers.CharField() + + def validate_tag(self, attrs, source): + tag = attrs.get(source, None) + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs + + +class MixTagsSerializer(ProjectTagSerializer): + from_tags = TagsField() + to_tag = serializers.CharField() + + def validate_from_tags(self, attrs, source): + tags = attrs.get(source, None) + for tag in tags: + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs + + def validate_to_tag(self, attrs, source): + tag = attrs.get(source, None) + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index a115275b..f2fcc3c0 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -57,3 +57,6 @@ from .stats import get_member_stats_for_project from .transfer import request_project_transfer, start_project_transfer from .transfer import accept_project_transfer, reject_project_transfer + +from .tags import tag_exist_for_project_elements, create_tag +from .tags import edit_tag, delete_tag, mix_tags diff --git a/taiga/projects/services/tags.py b/taiga/projects/services/tags.py new file mode 100644 index 00000000..20e1946a --- /dev/null +++ b/taiga/projects/services/tags.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.db import connection + +def tag_exist_for_project_elements(project, tag): + return tag in dict(project.tags_colors).keys() + + +def create_tag(project, tag, color): + project.tags_colors.append([tag, color]) + project.save() + + +def edit_tag(project, from_tag, to_tag=None, color=None): + tags_colors = dict(project.tags_colors) + + if color is not None: + tags_colors = dict(project.tags_colors) + tags_colors[from_tag] = color + + if to_tag is not None: + color = dict(project.tags_colors)[from_tag] + sql = """ + UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + """ + sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag) + cursor = connection.cursor() + cursor.execute(sql) + + tags_colors[to_tag] = tags_colors.pop(from_tag) + + + project.tags_colors = list(tags_colors.items()) + project.save() + + +def rename_tag(project, from_tag, to_tag): + color = dict(project.tags_colors)[from_tag] + sql = """ + UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + """ + sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color) + cursor = connection.cursor() + cursor.execute(sql) + + tags_colors = dict(project.tags_colors) + tags_colors[to_tag] = tags_colors.pop(from_tag) + project.tags_colors = list(tags_colors.items()) + project.save() + + +def delete_tag(project, tag): + sql = """ + UPDATE userstories_userstory SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id}; + UPDATE tasks_task SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id}; + UPDATE issues_issue SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id}; + """ + sql = sql.format(project_id=project.id, tag=tag) + cursor = connection.cursor() + cursor.execute(sql) + + tags_colors = dict(project.tags_colors) + del tags_colors[tag] + project.tags_colors = list(tags_colors.items()) + project.save() + + +def mix_tags(project, from_tags, to_tag): + for from_tag in from_tags: + rename_tag(project, from_tag, to_tag) diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index 0115143a..2e95f731 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -27,20 +27,24 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=['view_project'], public_permissions=['view_project'], - owner=m.project_owner) + owner=m.project_owner, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=['view_project'], public_permissions=['view_project'], - owner=m.project_owner) + owner=m.project_owner, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) + owner=m.project_owner, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, - blocked_code=project_choices.BLOCKED_BY_STAFF) + blocked_code=project_choices.BLOCKED_BY_STAFF, + tags_colors = [("tag1", "#123123"), ("tag2", "#456456"), ("tag3", "#111222")]) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -1911,3 +1915,127 @@ def test_project_template_patch(client, data): results = helper_test_http_method(client, 'patch', url, '{"name": "Test"}', users) assert results == [401, 403, 200] + + +def test_create_tag(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "tag": "testtest", + "color": "#123123" + }) + + url = reverse('projects-create-tag', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-create-tag', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-create-tag', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-create-tag', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] + + +def test_edit_tag(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "from_tag": "tag1", + "to_tag": "renamedtag1", + "color": "#123123" + }) + + url = reverse('projects-edit-tag', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-edit-tag', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-edit-tag', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-edit-tag', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] + + +def test_delete_tag(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "tag": "tag2", + }) + + url = reverse('projects-delete-tag', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-delete-tag', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-delete-tag', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-delete-tag', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] + + +def test_mix_tags(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "from_tags": ["tag1"], + "to_tag": "tag3" + }) + + url = reverse('projects-mix-tags', kwargs={"pk": data.public_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-mix-tags', kwargs={"pk": data.private_project1.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 200] + + url = reverse('projects-mix-tags', kwargs={"pk": data.private_project2.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 200] + + url = reverse('projects-mix-tags', kwargs={"pk": data.blocked_project.pk}) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [404, 404, 404, 403, 451] diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 4c2e121e..29d57c50 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -10,6 +10,9 @@ from taiga.projects.services import stats as stats_services from taiga.projects.history.services import take_snapshot from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.models import Project +from taiga.projects.userstories.models import UserStory +from taiga.projects.tasks.models import Task +from taiga.projects.issues.models import Issue from taiga.projects.choices import BLOCKED_BY_DELETING from .. import factories as f @@ -1854,6 +1857,189 @@ def test_delete_project_with_celery_disabled(client, settings): assert Project.objects.filter(id=project.id).count() == 0 +def test_create_tag(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-create-tag", args=(project.id,)) + client.login(user) + data = { + "tag": "newtag", + "color": "#123123" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["newtag", "#123123"]] + + +def test_create_tag_without_color(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-create-tag", args=(project.id,)) + client.login(user) + data = { + "tag": "newtag", + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors[0][0] == "newtag" + + +def test_edit_tag_only_name(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-edit-tag", args=(project.id,)) + client.login(user) + data = { + "from_tag": "tag", + "to_tag": "renamed_tag" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["renamed_tag", "#123123"]] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == ["renamed_tag"] + task = Task.objects.get(id=task.pk) + assert task.tags == ["renamed_tag"] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == ["renamed_tag"] + + +def test_edit_tag_only_color(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-edit-tag", args=(project.id,)) + client.login(user) + data = { + "from_tag": "tag", + "color": "#AAABBB" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["tag", "#AAABBB"]] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == ["tag"] + task = Task.objects.get(id=task.pk) + assert task.tags == ["tag"] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == ["tag"] + + +def test_edit_tag(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-edit-tag", args=(project.id,)) + client.login(user) + data = { + "from_tag": "tag", + "to_tag": "renamed_tag", + "color": "#AAABBB" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [["renamed_tag", "#AAABBB"]] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == ["renamed_tag"] + task = Task.objects.get(id=task.pk) + assert task.tags == ["renamed_tag"] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == ["renamed_tag"] + + +def test_delete_tag(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) + task = f.TaskFactory.create(project=project, tags=["tag"]) + issue = f.IssueFactory.create(project=project, tags=["tag"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-delete-tag", args=(project.id,)) + client.login(user) + data = { + "tag": "tag" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert project.tags_colors == [] + user_story = UserStory.objects.get(id=user_story.pk) + assert user_story.tags == [] + task = Task.objects.get(id=task.pk) + assert task.tags == [] + issue = Issue.objects.get(id=issue.pk) + assert issue.tags == [] + + +def test_mix_tags(client, settings): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, tags_colors=[("tag1", "#123123"), ("tag2", "#123123"), ("tag3", "#123123")]) + user_story = f.UserStoryFactory.create(project=project, tags=["tag1", "tag3"]) + task = f.TaskFactory.create(project=project, tags=["tag2", "tag3"]) + issue = f.IssueFactory.create(project=project, tags=["tag1", "tag2", "tag3"]) + + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) + url = reverse("projects-mix-tags", args=(project.id,)) + client.login(user) + data = { + "from_tags": ["tag1", "tag2"], + "to_tag": "tag2" + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 200 + project = Project.objects.get(id=project.pk) + assert set(["tag2", "tag3"]) == set(dict(project.tags_colors).keys()) + user_story = UserStory.objects.get(id=user_story.pk) + assert set(user_story.tags) == set(["tag2", "tag3"]) + task = Task.objects.get(id=task.pk) + assert set(task.tags) == set(["tag2", "tag3"]) + issue = Issue.objects.get(id=issue.pk) + assert set(issue.tags) == set(["tag2", "tag3"]) + + def test_color_tags_project_fired_on_element_create(): user_story = f.UserStoryFactory.create(tags=["tag"]) project = Project.objects.get(id=user_story.project.id) @@ -1875,4 +2061,3 @@ def test_color_tags_project_fired_on_element_update_respecting_color(): user_story.save() project = Project.objects.get(id=user_story.project.id) assert project.tags_colors == [["tag", "#123123"]] ->>>>>>> d64d158... WIP: migrations, removing automatic color generation diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index d65b2451..2c25b29d 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -481,7 +481,7 @@ def test_get_watched_list_valid_info_for_project(): fav_user = f.UserFactory() viewer_user = f.UserFactory() - project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) + project = f.ProjectFactory(is_private=False, name="Testing project") role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) project.add_watcher(fav_user) @@ -499,11 +499,6 @@ def test_get_watched_list_valid_info_for_project(): assert project_watch_info["assigned_to"] == None assert project_watch_info["status"] == None assert project_watch_info["status_color"] == None - - tags_colors = {tc["name"]:tc["color"] for tc in project_watch_info["tags_colors"]} - assert "test" in tags_colors - assert "tag" in tags_colors - assert project_watch_info["is_private"] == project.is_private assert project_watch_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL) assert project_watch_info["is_fan"] == False @@ -540,7 +535,7 @@ def test_get_liked_list_valid_info(): fan_user = f.UserFactory() viewer_user = f.UserFactory() - project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) + project = f.ProjectFactory(is_private=False, name="Testing project") content_type = ContentType.objects.get_for_model(project) like = f.LikeFactory(content_type=content_type, object_id=project.id, user=fan_user) project.refresh_totals() @@ -558,11 +553,6 @@ def test_get_liked_list_valid_info(): assert project_like_info["assigned_to"] == None assert project_like_info["status"] == None assert project_like_info["status_color"] == None - - tags_colors = {tc["name"]:tc["color"] for tc in project_like_info["tags_colors"]} - assert "test" in tags_colors - assert "tag" in tags_colors - assert project_like_info["is_private"] == project.is_private assert project_like_info["logo_small_url"] == get_thumbnail_url(project.logo, settings.THN_LOGO_SMALL) From 8c45033f18d9208c1a1d2de3942210f8058c4874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 8 Jun 2016 21:26:09 +0200 Subject: [PATCH 047/261] Assign a color when create a new tag --- taiga/base/fields.py | 31 +---- taiga/projects/api.py | 2 +- taiga/projects/apps.py | 3 +- taiga/projects/issues/api.py | 9 +- taiga/projects/issues/apps.py | 3 +- taiga/projects/issues/serializers.py | 15 ++- taiga/projects/serializers.py | 13 +- taiga/projects/services/tags.py | 1 + taiga/projects/signals.py | 7 - taiga/projects/tagging/__init__.py | 0 taiga/projects/tagging/fields.py | 99 ++++++++++++++ taiga/projects/tagging/mixins.py | 48 +++++++ taiga/projects/tagging/signals.py | 23 ++++ taiga/projects/tasks/api.py | 20 +-- taiga/projects/tasks/apps.py | 4 +- taiga/projects/tasks/serializers.py | 23 ++-- taiga/projects/userstories/api.py | 29 +++-- taiga/projects/userstories/apps.py | 3 +- taiga/projects/userstories/serializers.py | 24 ++-- taiga/webhooks/serializers.py | 3 +- tests/integration/test_issues_tags.py | 142 +++++++++++++++++++++ tests/integration/test_tasks.py | 13 -- tests/integration/test_tasks_tags.py | 142 +++++++++++++++++++++ tests/integration/test_userstories_tags.py | 142 +++++++++++++++++++++ 24 files changed, 680 insertions(+), 119 deletions(-) create mode 100644 taiga/projects/tagging/__init__.py create mode 100644 taiga/projects/tagging/fields.py create mode 100644 taiga/projects/tagging/mixins.py create mode 100644 taiga/projects/tagging/signals.py create mode 100644 tests/integration/test_issues_tags.py create mode 100644 tests/integration/test_tasks_tags.py create mode 100644 tests/integration/test_userstories_tags.py diff --git a/taiga/base/fields.py b/taiga/base/fields.py index 3f6fcf19..8e95801d 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -18,7 +18,7 @@ from django.forms import widgets from django.utils.translation import ugettext as _ - +from django.utils.translation import ugettext_lazy from taiga.base.api import serializers @@ -99,35 +99,6 @@ class PickledObjectField(serializers.WritableField): return data -class TagsField(serializers.WritableField): - """ - Pickle objects serializer. - """ - def to_native(self, obj): - return obj - - def from_native(self, data): - if not data: - return data - - ret = sum([tag.split(",") for tag in data], []) - return ret - - -class TagsColorsField(serializers.WritableField): - """ - PgArray objects serializer. - """ - widget = widgets.Textarea - - def to_native(self, obj): - return dict(obj) - - def from_native(self, data): - return list(data.items()) - - - class WatchersField(serializers.WritableField): def to_native(self, obj): return obj diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 958ed3b3..77d9a5d1 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -66,9 +66,9 @@ from . import services ###################################################### ## Project ###################################################### + class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet): - queryset = models.Project.objects.all() serializer_class = serializers.ProjectDetailSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer diff --git a/taiga/projects/apps.py b/taiga/projects/apps.py index 38295e81..634d56ce 100644 --- a/taiga/projects/apps.py +++ b/taiga/projects/apps.py @@ -25,13 +25,14 @@ from django.db.models import signals def connect_projects_signals(): from . import signals as handlers + from .tagging import signals as tagging_handlers # On project object is created apply template. signals.post_save.connect(handlers.project_post_save, sender=apps.get_model("projects", "Project"), dispatch_uid='project_post_save') # Tags normalization after save a project - signals.pre_save.connect(handlers.tags_normalization, + signals.pre_save.connect(tagging_handlers.tags_normalization, sender=apps.get_model("projects", "Project"), dispatch_uid="tags_normalization_projects") diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index b368ec8d..20befba7 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -27,11 +27,11 @@ from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 +from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin -from taiga.projects.history.mixins import HistoryResourceMixin - -from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType +from taiga.projects.tagging.mixins import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -41,7 +41,7 @@ from . import serializers class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - BlockedByProjectMixin, ModelCrudViewSet): + TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) filter_backends = (filters.CanViewIssuesFilterBackend, @@ -196,7 +196,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) priorities_filter_backends = (f for f in filter_backends if f != filters.PrioritiesFilter) severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter) - tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter) queryset = self.get_queryset() querysets = { diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index 671a45be..ac01491a 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -23,6 +23,7 @@ from django.db.models import signals def connect_issues_signals(): from taiga.projects import signals as generic_handlers + from taiga.projects.tagging import signals as tagging_handlers from . import signals as handlers # Finished date @@ -31,7 +32,7 @@ def connect_issues_signals(): dispatch_uid="set_finished_date_when_edit_issue") # Tags - signals.pre_save.connect(generic_handlers.tags_normalization, + signals.pre_save.connect(tagging_handlers.tags_normalization, sender=apps.get_model("issues", "Issue"), dispatch_uid="tags_normalization_issue") diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 505f8463..83557f20 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -17,15 +17,15 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.base.fields import TagsField from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin -from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicIssueStatusSerializer -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.mdrender.service import render as mdrender +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.validators import ProjectExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.users.serializers import UserBasicInfoSerializer @@ -33,8 +33,9 @@ from taiga.users.serializers import UserBasicInfoSerializer from . import models -class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): - tags = TagsField(required=False) +class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): + tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) is_closed = serializers.Field(source="is_closed") comment = serializers.SerializerMethodField("get_comment") @@ -71,7 +72,7 @@ class IssueListSerializer(IssueSerializer): class Meta: model = models.Issue read_only_fields = ('id', 'ref', 'created_date', 'modified_date') - exclude=("description", "description_html") + exclude = ("description", "description_html") class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index f7403389..f18bee1d 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -24,28 +24,27 @@ from django.db.models import Q from taiga.base.api import serializers from taiga.base.fields import JsonField from taiga.base.fields import PgArrayField -from taiga.base.fields import TagsField -from taiga.base.fields import TagsColorsField -from taiga.projects.notifications.choices import NotifyLevel from taiga.users.services import get_photo_or_gravatar_url -from taiga.users.serializers import UserSerializer from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import ProjectRoleSerializer from taiga.users.validators import RoleExistsValidator from taiga.permissions.services import get_user_project_permissions from taiga.permissions.services import is_project_admin, is_project_owner -from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin from . import models from . import services -from .notifications.mixins import WatchedResourceModelSerializer -from .validators import ProjectExistsValidator from .custom_attributes.serializers import UserStoryCustomAttributeSerializer from .custom_attributes.serializers import TaskCustomAttributeSerializer from .custom_attributes.serializers import IssueCustomAttributeSerializer from .likes.mixins.serializers import FanResourceSerializerMixin +from .mixins.serializers import ValidateDuplicatedNameInProjectMixin +from .notifications.choices import NotifyLevel +from .notifications.mixins import WatchedResourceModelSerializer +from .tagging.fields import TagsField +from .tagging.fields import TagsColorsField +from .validators import ProjectExistsValidator ###################################################### diff --git a/taiga/projects/services/tags.py b/taiga/projects/services/tags.py index 20e1946a..ea009df7 100644 --- a/taiga/projects/services/tags.py +++ b/taiga/projects/services/tags.py @@ -18,6 +18,7 @@ from django.db import connection + def tag_exist_for_project_elements(project, tag): return tag in dict(project.tags_colors).keys() diff --git a/taiga/projects/signals.py b/taiga/projects/signals.py index e0196887..b94e5cda 100644 --- a/taiga/projects/signals.py +++ b/taiga/projects/signals.py @@ -29,13 +29,6 @@ from easy_thumbnails.files import get_thumbnailer # Signals over project items #################################### -## TAGS - -def tags_normalization(sender, instance, **kwargs): - if isinstance(instance.tags, (list, tuple)): - instance.tags = list(map(str.lower, instance.tags)) - - ## Membership def membership_post_delete(sender, instance, using, **kwargs): diff --git a/taiga/projects/tagging/__init__.py b/taiga/projects/tagging/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/tagging/fields.py b/taiga/projects/tagging/fields.py new file mode 100644 index 00000000..b56e3cc1 --- /dev/null +++ b/taiga/projects/tagging/fields.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.forms import widgets +from django.utils.translation import ugettext_lazy as _ +from taiga.base.api import serializers + +from django.core.exceptions import ValidationError + +import re + + +class TagsAndTagsColorsField(serializers.WritableField): + """ + Pickle objects serializer fior stories, tasks and issues tags. + """ + def __init__(self, *args, **kwargs): + def _validate_tag_field(value): + # Valid field: + # - ["tag1", "tag2", "tag3"...] + # - ["tag1", ["tag2", None], ["tag3", "#ccc"], [tag4, #cccccc]...] + for tag in value: + if isinstance(tag, str): + continue + + if isinstance(tag, (list, tuple)) and len(tag) == 2: + name = tag[0] + color = tag[1] + + if isinstance(name, str): + if color is None: + continue + + if isinstance(color, str) and re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + continue + + raise ValidationError(_("Invalid tag '{value}'. The color is not a " + "valid HEX color or null.").format(value=tag)) + + raise ValidationError(_("Invalid tag '{value}'. it must be the name or a pair " + "'[\"name\", \"hex color/\" | null]'.").format(value=tag)) + + super().__init__(*args, **kwargs) + self.validators.append(_validate_tag_field) + + def to_native(self, obj): + return obj + + def from_native(self, data): + return data + + +class TagsField(serializers.WritableField): + """ + Pickle objects serializer for tags names. + """ + def __init__(self, *args, **kwargs): + def _validate_tag_field(value): + for tag in value: + if isinstance(tag, str): + continue + raise ValidationError(_("Invalid tag '{value}'. It must be the tag name.").format(value=tag)) + + super().__init__(*args, **kwargs) + self.validators.append(_validate_tag_field) + + def to_native(self, obj): + return obj + + def from_native(self, data): + return data + + +class TagsColorsField(serializers.WritableField): + """ + PgArray objects serializer. + """ + widget = widgets.Textarea + + def to_native(self, obj): + return dict(obj) + + def from_native(self, data): + return list(data.items()) diff --git a/taiga/projects/tagging/mixins.py b/taiga/projects/tagging/mixins.py new file mode 100644 index 00000000..aa5df99f --- /dev/null +++ b/taiga/projects/tagging/mixins.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + + +def _pre_save_new_tags_in_project_tagss_colors(obj): + current_project_tags = [t[0] for t in obj.project.tags_colors] + new_obj_tags = set() + new_tags_colors = {} + + for tag in obj.tags: + if isinstance(tag, (list, tuple)): + name, color = tag + + if color and name not in current_project_tags: + new_tags_colors[name] = color + + new_obj_tags.add(name) + elif isinstance(tag, str): + new_obj_tags.add(tag.lower()) + + obj.tags = list(new_obj_tags) + + if new_tags_colors: + obj.project.tags_colors += [[k, v] for k,v in new_tags_colors.items()] + obj.project.save(update_fields=["tags_colors"]) + + +class TaggedResourceMixin: + def pre_save(self, obj): + if obj.tags: + _pre_save_new_tags_in_project_tagss_colors(obj) + + super().pre_save(obj) diff --git a/taiga/projects/tagging/signals.py b/taiga/projects/tagging/signals.py new file mode 100644 index 00000000..562fcba5 --- /dev/null +++ b/taiga/projects/tagging/signals.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + + +def tags_normalization(sender, instance, **kwargs): + if isinstance(instance.tags, (list, tuple)): + instance.tags = list(map(str.lower, instance.tags)) + diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index d991b39b..7b5de5f3 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.http import HttpResponse from django.utils.translation import ugettext as _ from taiga.base.api.utils import get_object_or_404 @@ -24,15 +25,13 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.projects.models import Project, TaskStatus -from django.http import HttpResponse - -from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.models import Project, TaskStatus +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin +from taiga.projects.tagging.mixins import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin - from . import models from . import permissions from . import serializers @@ -40,13 +39,18 @@ from . import services class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - BlockedByProjectMixin, ModelCrudViewSet): + TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter) retrieve_exclude_filters = (filters.WatchersFilter,) - filter_fields = ["user_story", "milestone", "project", "assigned_to", - "status__is_closed"] + filter_fields = [ + "user_story", + "milestone", + "project", + "assigned_to", + "status__is_closed" + ] def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 23cfdfb0..7ae193cc 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -23,13 +23,15 @@ from django.db.models import signals def connect_tasks_signals(): from taiga.projects import signals as generic_handlers + from taiga.projects.tagging import signals as tagging_handlers from . import signals as handlers + # Finished date signals.pre_save.connect(handlers.set_finished_date_when_edit_task, sender=apps.get_model("tasks", "Task"), dispatch_uid="set_finished_date_when_edit_task") # Tags - signals.pre_save.connect(generic_handlers.tags_normalization, + signals.pre_save.connect(tagging_handlers.tags_normalization, sender=apps.get_model("tasks", "Task"), dispatch_uid="tags_normalization_task") diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index a7c1c2a8..c0c8334a 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -17,19 +17,18 @@ # along with this program. If not, see . from taiga.base.api import serializers - -from taiga.base.fields import TagsField from taiga.base.fields import PgArrayField - from taiga.base.neighbors import NeighborsSerializerMixin -from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator + from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.tasks.validators import TaskExistsValidator +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicTaskStatusSerializerSerializer -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.mdrender.service import render as mdrender +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.tasks.validators import TaskExistsValidator +from taiga.projects.validators import ProjectExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.users.serializers import UserBasicInfoSerializer @@ -37,14 +36,15 @@ from taiga.users.serializers import UserBasicInfoSerializer from . import models -class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): - tags = TagsField(required=False, default=[]) +class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, + serializers.ModelSerializer): + tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) comment = serializers.SerializerMethodField("get_comment") milestone_slug = serializers.SerializerMethodField("get_milestone_slug") blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") description_html = serializers.SerializerMethodField("get_description_html") - is_closed = serializers.SerializerMethodField("get_is_closed") + is_closed = serializers.SerializerMethodField("get_is_closed") status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True) assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) @@ -76,7 +76,7 @@ class TaskListSerializer(TaskSerializer): class Meta: model = models.Task read_only_fields = ('id', 'ref', 'created_date', 'modified_date') - exclude=("description", "description_html") + exclude = ("description", "description_html") class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer): @@ -101,6 +101,7 @@ class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, us_id = serializers.IntegerField(required=False) bulk_tasks = serializers.CharField() + ## Order bulk serializers class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer): diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index c3858555..028bfe35 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -19,7 +19,6 @@ from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ -from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse from taiga.base import filters @@ -31,12 +30,13 @@ from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.utils import get_object_or_404 -from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin -from taiga.projects.occ import OCCResourceMixin -from taiga.projects.models import Project, UserStoryStatus -from taiga.projects.milestones.models import Milestone from taiga.projects.history.services import take_snapshot +from taiga.projects.milestones.models import Milestone +from taiga.projects.models import Project, UserStoryStatus +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin +from taiga.projects.occ import OCCResourceMixin +from taiga.projects.tagging.mixins import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -46,7 +46,7 @@ from . import services class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - BlockedByProjectMixin, ModelCrudViewSet): + TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) filter_backends = (filters.CanViewUsFilterBackend, @@ -113,8 +113,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def pre_save(self, obj): # This is very ugly hack, but having # restframework is the only way to do it. + # # NOTE: code moved as is from serializer - # to api because is not serializer logic. + # to api because is not serializer logic. related_data = getattr(obj, "_related_data", {}) self._role_points = related_data.pop("role_points", None) @@ -124,7 +125,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi super().pre_save(obj) def post_save(self, obj, created=False): - # Code related to the hack of pre_save method. Rather, this is the continuation of it. + # Code related to the hack of pre_save method. + # Rather, this is the continuation of it. if self._role_points: Points = apps.get_model("projects", "Points") RolePoints = apps.get_model("userstories", "RolePoints") @@ -134,14 +136,16 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk, role__computable=True) except (ValueError, RolePoints.DoesNotExist): - raise exc.BadRequest({"points": _("Invalid role id '{role_id}'").format( - role_id=role_id)}) + raise exc.BadRequest({ + "points": _("Invalid role id '{role_id}'").format(role_id=role_id) + }) try: role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id) except (ValueError, Points.DoesNotExist): - raise exc.BadRequest({"points": _("Invalid points id '{points_id}'").format( - points_id=points_id)}) + raise exc.BadRequest({ + "points": _("Invalid points id '{points_id}'").format(points_id=points_id) + }) role_points.save() @@ -200,7 +204,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) - tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter) queryset = self.get_queryset() querysets = { diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index 04c7d32d..fca31409 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -23,6 +23,7 @@ from django.db.models import signals def connect_userstories_signals(): from taiga.projects import signals as generic_handlers + from taiga.projects.tagging import signals as tagging_handlers from . import signals as handlers # When deleting user stories we must disable task signals while delating and @@ -59,7 +60,7 @@ def connect_userstories_signals(): dispatch_uid="try_to_close_milestone_when_delete_us") # Tags - signals.pre_save.connect(generic_handlers.tags_normalization, + signals.pre_save.connect(tagging_handlers.tags_normalization, sender=apps.get_model("userstories", "UserStory"), dispatch_uid="tags_normalization_user_story") diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 0d6eab6d..dae58a18 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -16,23 +16,22 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.apps import apps from taiga.base.api import serializers from taiga.base.api.utils import get_object_or_404 -from taiga.base.fields import TagsField from taiga.base.fields import PickledObjectField from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.utils import json -from taiga.mdrender.service import render as mdrender -from taiga.projects.models import Project -from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.userstories.validators import UserStoryExistsValidator +from taiga.projects.models import Project from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.serializers import BasicUserStoryStatusSerializer from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.serializers import BasicUserStoryStatusSerializer +from taiga.mdrender.service import render as mdrender +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.userstories.validators import UserStoryExistsValidator +from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.users.serializers import UserBasicInfoSerializer @@ -50,9 +49,9 @@ class RolePointsField(serializers.WritableField): return json.loads(obj) -class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - tags = TagsField(default=[], required=False) +class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, + EditableWatchedResourceModelSerializer, serializers.ModelSerializer): + tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) points = RolePointsField(source="role_points", required=False) total_points = serializers.SerializerMethodField("get_total_points") @@ -112,7 +111,7 @@ class UserStoryListSerializer(UserStorySerializer): model = models.UserStory depth = 0 read_only_fields = ('created_date', 'modified_date') - exclude=("description", "description_html") + exclude = ("description", "description_html") class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): @@ -142,7 +141,8 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serial order = serializers.IntegerField() -class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer): +class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, + serializers.Serializer): project_id = serializers.IntegerField() bulk_stories = _UserStoryOrderBulkSerializer(many=True) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index 68d6a8eb..ee0d8308 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -19,7 +19,7 @@ from django.core.exceptions import ObjectDoesNotExist from taiga.base.api import serializers -from taiga.base.fields import TagsField, PgArrayField, JsonField +from taiga.base.fields import PgArrayField, JsonField from taiga.front.templatetags.functions import resolve as resolve_front_url @@ -29,6 +29,7 @@ from taiga.projects.milestones import models as milestone_models from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.services import get_logo_big_thumbnail_url from taiga.projects.tasks import models as task_models +from taiga.projects.tagging.fields import TagsField from taiga.projects.userstories import models as us_models from taiga.projects.wiki import models as wiki_models diff --git a/tests/integration/test_issues_tags.py b/tests/integration/test_issues_tags.py new file mode 100644 index 00000000..5a38bab0 --- /dev/null +++ b/tests/integration/test_issues_tags.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +from unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_issue_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + issue = f.create_issue(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True) + url = reverse("issues-detail", kwargs={"pk": issue.pk}) + data = { + "tags": [], + "version": issue.version + } + + client.login(issue.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_issue_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + issue = f.create_issue(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True) + url = reverse("issues-detail", kwargs={"pk": issue.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": issue.version + } + + client.login(issue.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_issue_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + issue = f.create_issue(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=issue.owner, is_admin=True) + url = reverse("issues-detail", kwargs={"pk": issue.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": issue.version + } + + client.login(issue.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_issue_with_tags(client): + project = f.ProjectFactory.create() + status = f.IssueStatusFactory.create(project=project) + project.default_issue_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("issues-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + assert ("back" in response.data["tags"] and + "front" in response.data["tags"] and + "ux" in response.data["tags"]) + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index bb077c2f..712fa07e 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -67,19 +67,6 @@ def test_create_task_without_default_values(client): assert response.data['status'] == None -def test_api_update_task_tags(client): - project = f.ProjectFactory.create() - task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) - f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) - url = reverse("tasks-detail", kwargs={"pk": task.pk}) - data = {"tags": ["back", "front"], "version": task.version} - - client.login(task.owner) - response = client.json.patch(url, json.dumps(data)) - - assert response.status_code == 200, response.data - - def test_api_create_in_bulk_with_status(client): us = f.create_userstory() f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) diff --git a/tests/integration/test_tasks_tags.py b/tests/integration/test_tasks_tags.py new file mode 100644 index 00000000..67a27c0d --- /dev/null +++ b/tests/integration/test_tasks_tags.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +from unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_task_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) + url = reverse("tasks-detail", kwargs={"pk": task.pk}) + data = { + "tags": [], + "version": task.version + } + + client.login(task.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_task_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) + url = reverse("tasks-detail", kwargs={"pk": task.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": task.version + } + + client.login(task.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_task_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + task = f.create_task(project=project, status__project=project, milestone=None, user_story=None) + f.MembershipFactory.create(project=project, user=task.owner, is_admin=True) + url = reverse("tasks-detail", kwargs={"pk": task.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": task.version + } + + client.login(task.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_task_with_tags(client): + project = f.ProjectFactory.create() + status = f.TaskStatusFactory.create(project=project) + project.default_task_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("tasks-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + assert ("back" in response.data["tags"] and + "front" in response.data["tags"] and + "ux" in response.data["tags"]) + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" diff --git a/tests/integration/test_userstories_tags.py b/tests/integration/test_userstories_tags.py new file mode 100644 index 00000000..1313dc91 --- /dev/null +++ b/tests/integration/test_userstories_tags.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +from unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_user_story_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + user_story = f.create_userstory(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True) + url = reverse("userstories-detail", kwargs={"pk": user_story.pk}) + data = { + "tags": [], + "version": user_story.version + } + + client.login(user_story.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_user_story_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + user_story = f.create_userstory(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True) + url = reverse("userstories-detail", kwargs={"pk": user_story.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": user_story.version + } + + client.login(user_story.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_user_story_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + user_story = f.create_userstory(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=user_story.owner, is_admin=True) + url = reverse("userstories-detail", kwargs={"pk": user_story.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": user_story.version + } + + client.login(user_story.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_user_story_with_tags(client): + project = f.ProjectFactory.create() + status = f.UserStoryStatusFactory.create(project=project) + project.default_userstory_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("userstories-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + assert ("back" in response.data["tags"] and + "front" in response.data["tags"] and + "ux" in response.data["tags"]) + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" From 2b82bb056f17d788ebda8fbd7e35df2d33f1c388 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 9 Jun 2016 07:41:17 +0200 Subject: [PATCH 048/261] Updating CHANGELOG --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a82a19..67c30c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,15 @@ ## 2.2.0 ??? (unreleased) ### Features -- Now comment owners and project admins can edit existing comments with the history Entry endpoint. -- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. -- Include created, modified and finished dates for tasks in CSV reports +- Include created, modified and finished dates for tasks in CSV reports. - Add gravatar url to Users API endpoint. +- Comments: + - Now comment owners and project admins can edit existing comments with the history Entry endpoint. + - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. +- Tags: + - New API endpoints over projects to create, rename, edit, delete and mix tags. + - Tag color assignation is not automatic. + - Select a color (or not) to a tag when add it to stories, issues and tasks. ### Misc - Lots of small and not so small bugfixes. From b4c81f9c9db84ec29fda84e99bf9e8a245d96f9a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 9 Jun 2016 14:39:13 +0200 Subject: [PATCH 049/261] Fixing sample_data --- taiga/projects/management/commands/sample_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index db7a696c..937b11d7 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -260,6 +260,7 @@ class Command(BaseCommand): project_stats = get_stats_for_project(project) defined_points = project_stats["defined_points"] project.total_story_points = int(defined_points * self.sd.int(5,12) / 10) + project.refresh_from_db() project.save() self.create_likes(project) From fde98473c4b0f3ed18d18cf6fd0c6833870166fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 9 Jun 2016 19:18:52 +0200 Subject: [PATCH 050/261] Keep the to_tag color on mix_tags service function --- taiga/projects/services/tags.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/taiga/projects/services/tags.py b/taiga/projects/services/tags.py index ea009df7..010a23fb 100644 --- a/taiga/projects/services/tags.py +++ b/taiga/projects/services/tags.py @@ -53,8 +53,8 @@ def edit_tag(project, from_tag, to_tag=None, color=None): project.save() -def rename_tag(project, from_tag, to_tag): - color = dict(project.tags_colors)[from_tag] +def rename_tag(project, from_tag, to_tag, color=None): + color = color or dict(project.tags_colors)[from_tag] sql = """ UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; @@ -65,7 +65,8 @@ def rename_tag(project, from_tag, to_tag): cursor.execute(sql) tags_colors = dict(project.tags_colors) - tags_colors[to_tag] = tags_colors.pop(from_tag) + tags_colors.pop(from_tag) + tags_colors[to_tag] = color project.tags_colors = list(tags_colors.items()) project.save() @@ -87,5 +88,6 @@ def delete_tag(project, tag): def mix_tags(project, from_tags, to_tag): + color = dict(project.tags_colors)[to_tag] for from_tag in from_tags: - rename_tag(project, from_tag, to_tag) + rename_tag(project, from_tag, to_tag, color) From 13ef1b9af512ac403650adc4cc45de9446692c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 10 Jun 2016 18:39:28 +0200 Subject: [PATCH 051/261] Move all tags code to projects.tags --- taiga/projects/api.py | 89 ++----------- taiga/projects/issues/api.py | 2 +- taiga/projects/serializers.py | 93 ------------- taiga/projects/services/__init__.py | 3 - taiga/projects/tagging/api.py | 125 ++++++++++++++++++ taiga/projects/tagging/fields.py | 4 +- taiga/projects/tagging/mixins.py | 48 ------- taiga/projects/tagging/serializers.py | 112 ++++++++++++++++ .../{services/tags.py => tagging/services.py} | 55 ++++++-- taiga/projects/tagging/signals.py | 1 - taiga/projects/tasks/api.py | 2 +- taiga/projects/userstories/api.py | 2 +- 12 files changed, 297 insertions(+), 239 deletions(-) create mode 100644 taiga/projects/tagging/api.py delete mode 100644 taiga/projects/tagging/mixins.py create mode 100644 taiga/projects/tagging/serializers.py rename taiga/projects/{services/tags.py => tagging/services.py} (60%) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 77d9a5d1..6c7a4ec9 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -22,38 +22,38 @@ from dateutil.relativedelta import relativedelta from django.apps import apps from django.conf import settings +from django.core.exceptions import ValidationError from django.db.models import signals, Prefetch from django.db.models import Value as V from django.db.models.functions import Coalesce -from django.core.exceptions import ValidationError +from django.http import Http404 from django.utils.translation import ugettext as _ from django.utils import timezone -from django.http import Http404 from taiga.base import filters -from taiga.base import response from taiga.base import exceptions as exc -from taiga.base.decorators import list_route -from taiga.base.decorators import detail_route +from taiga.base import response from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin, BlockeableSaveMixin, BlockeableDeleteMixin from taiga.base.api.permissions import AllowAnyPermission from taiga.base.api.utils import get_object_or_404 +from taiga.base.decorators import list_route +from taiga.base.decorators import detail_route from taiga.base.utils.slug import slugify_uniquely +from taiga.permissions import services as permissions_services from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.issues.models import Issue +from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin from taiga.projects.notifications.models import NotifyPolicy from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.notifications.choices import NotifyLevel - -from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin +from taiga.projects.mixins.ordering import BulkUpdateOrderMixin +from taiga.projects.tasks.models import Task +from taiga.projects.tagging.api import TagsColorsResourceMixin from taiga.projects.userstories.models import UserStory, RolePoints -from taiga.projects.tasks.models import Task -from taiga.projects.issues.models import Issue -from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin -from taiga.permissions import services as permissions_services from taiga.users import services as users_services from . import filters as project_filters @@ -67,8 +67,8 @@ from . import services ## Project ###################################################### -class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, - BlockeableSaveMixin, BlockeableDeleteMixin, ModelCrudViewSet): +class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin, + TagsColorsResourceMixin, ModelCrudViewSet): queryset = models.Project.objects.all() serializer_class = serializers.ProjectDetailSerializer admin_serializer_class = serializers.ProjectDetailAdminSerializer @@ -327,12 +327,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "issues_stats", project) return response.Ok(services.get_stats_for_project_issues(project)) - @detail_route(methods=["GET"]) - def tags_colors(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "tags_colors", project) - return response.Ok(dict(project.tags_colors)) - @detail_route(methods=["POST"]) def transfer_validate_token(self, request, pk=None): project = self.get_object() @@ -405,63 +399,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, services.reject_project_transfer(project, request.user, token, reason) return response.Ok() - @detail_route(methods=["POST"]) - def create_tag(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "create_tag", project) - self._raise_if_blocked(project) - serializer = serializers.CreateTagSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - services.create_tag(project, data.get("tag"), data.get("color")) - return response.Ok() - - - @detail_route(methods=["POST"]) - def edit_tag(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "edit_tag", project) - self._raise_if_blocked(project) - serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - services.edit_tag(project, data.get("from_tag"), - to_tag=data.get("to_tag", None), - color=data.get("color", None)) - - return response.Ok() - - - @detail_route(methods=["POST"]) - def delete_tag(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "delete_tag", project) - self._raise_if_blocked(project) - serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - services.delete_tag(project, data.get("tag")) - return response.Ok() - - @detail_route(methods=["POST"]) - def mix_tags(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "mix_tags", project) - self._raise_if_blocked(project) - serializer = serializers.MixTagsSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - services.mix_tags(project, data.get("from_tags"), data.get("to_tag")) - return response.Ok() - def _raise_if_blocked(self, project): if self.is_blocked(project): raise exc.Blocked(_("Blocked element")) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 20befba7..cae23be3 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -31,7 +31,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, IssueStatus, Severity, Priority, IssueType from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin -from taiga.projects.tagging.mixins import TaggedResourceMixin +from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index f18bee1d..f388528f 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import re - from django.utils.translation import ugettext as _ from django.db.models import Q @@ -416,94 +414,3 @@ class ProjectTemplateSerializer(serializers.ModelSerializer): class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer): project_id = serializers.IntegerField() order = serializers.IntegerField() - - -###################################################### -## Project tags serializers -###################################################### - - -class ProjectTagSerializer(serializers.Serializer): - def __init__(self, *args, **kwargs): - # Don't pass the extra project arg - self.project = kwargs.pop("project") - - # Instantiate the superclass normally - super().__init__(*args, **kwargs) - - -class CreateTagSerializer(ProjectTagSerializer): - tag = serializers.CharField() - color = serializers.CharField(required=False) - - def validate_tag(self, attrs, source): - tag = attrs.get(source, None) - if services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag exists.")) - - return attrs - - def validate_color(self, attrs, source): - color = attrs.get(source, None) - if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): - raise serializers.ValidationError(_("The color is not a valid HEX color.")) - - return attrs - - -class EditTagTagSerializer(ProjectTagSerializer): - from_tag = serializers.CharField() - to_tag = serializers.CharField(required=False) - color = serializers.CharField(required=False) - - def validate_from_tag(self, attrs, source): - tag = attrs.get(source, None) - if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) - - return attrs - - def validate_to_tag(self, attrs, source): - tag = attrs.get(source, None) - if services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag exists yet")) - - return attrs - - def validate_color(self, attrs, source): - color = attrs.get(source, None) - if len(color) != 7 or not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): - raise serializers.ValidationError(_("The color is not a valid HEX color.")) - - return attrs - - -class DeleteTagSerializer(ProjectTagSerializer): - tag = serializers.CharField() - - def validate_tag(self, attrs, source): - tag = attrs.get(source, None) - if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) - - return attrs - - -class MixTagsSerializer(ProjectTagSerializer): - from_tags = TagsField() - to_tag = serializers.CharField() - - def validate_from_tags(self, attrs, source): - tags = attrs.get(source, None) - for tag in tags: - if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) - - return attrs - - def validate_to_tag(self, attrs, source): - tag = attrs.get(source, None) - if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) - - return attrs diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index f2fcc3c0..a115275b 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -57,6 +57,3 @@ from .stats import get_member_stats_for_project from .transfer import request_project_transfer, start_project_transfer from .transfer import accept_project_transfer, reject_project_transfer - -from .tags import tag_exist_for_project_elements, create_tag -from .tags import edit_tag, delete_tag, mix_tags diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py new file mode 100644 index 00000000..d93ebe72 --- /dev/null +++ b/taiga/projects/tagging/api.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base import response +from taiga.base.decorators import detail_route + +from . import services +from . import serializers + + +class TagsColorsResourceMixin: + @detail_route(methods=["GET"]) + def tags_colors(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "tags_colors", project) + + return response.Ok(dict(project.tags_colors)) + + @detail_route(methods=["POST"]) + def create_tag(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "create_tag", project) + self._raise_if_blocked(project) + + serializer = serializers.CreateTagSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.create_tag(project, data.get("tag"), data.get("color")) + + return response.Ok() + + + @detail_route(methods=["POST"]) + def edit_tag(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "edit_tag", project) + self._raise_if_blocked(project) + + serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.edit_tag(project, + data.get("from_tag"), + to_tag=data.get("to_tag", None), + color=data.get("color", None)) + + return response.Ok() + + + @detail_route(methods=["POST"]) + def delete_tag(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "delete_tag", project) + self._raise_if_blocked(project) + + serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.delete_tag(project, data.get("tag")) + + return response.Ok() + + @detail_route(methods=["POST"]) + def mix_tags(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "mix_tags", project) + self._raise_if_blocked(project) + + serializer = serializers.MixTagsSerializer(data=request.DATA, project=project) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + services.mix_tags(project, data.get("from_tags"), data.get("to_tag")) + + return response.Ok() + + +class TaggedResourceMixin: + def pre_save(self, obj): + if obj.tags: + self._pre_save_new_tags_in_project_tagss_colors(obj) + + super().pre_save(obj) + + def _pre_save_new_tags_in_project_tagss_colors(self, obj): + new_obj_tags = set() + new_tags_colors = {} + + for tag in obj.tags: + if isinstance(tag, (list, tuple)): + name, color = tag + + if color and not services.tag_exist_for_project_elements(obj.project, name): + new_tags_colors[name] = color + + new_obj_tags.add(name) + elif isinstance(tag, str): + new_obj_tags.add(tag.lower()) + + obj.tags = list(new_obj_tags) + + if new_tags_colors: + services.create_tags(obj.project, new_tags_colors) diff --git a/taiga/projects/tagging/fields.py b/taiga/projects/tagging/fields.py index b56e3cc1..24f92f23 100644 --- a/taiga/projects/tagging/fields.py +++ b/taiga/projects/tagging/fields.py @@ -16,11 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.core.exceptions import ValidationError from django.forms import widgets from django.utils.translation import ugettext_lazy as _ -from taiga.base.api import serializers -from django.core.exceptions import ValidationError +from taiga.base.api import serializers import re diff --git a/taiga/projects/tagging/mixins.py b/taiga/projects/tagging/mixins.py deleted file mode 100644 index aa5df99f..00000000 --- a/taiga/projects/tagging/mixins.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# 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 . - - -def _pre_save_new_tags_in_project_tagss_colors(obj): - current_project_tags = [t[0] for t in obj.project.tags_colors] - new_obj_tags = set() - new_tags_colors = {} - - for tag in obj.tags: - if isinstance(tag, (list, tuple)): - name, color = tag - - if color and name not in current_project_tags: - new_tags_colors[name] = color - - new_obj_tags.add(name) - elif isinstance(tag, str): - new_obj_tags.add(tag.lower()) - - obj.tags = list(new_obj_tags) - - if new_tags_colors: - obj.project.tags_colors += [[k, v] for k,v in new_tags_colors.items()] - obj.project.save(update_fields=["tags_colors"]) - - -class TaggedResourceMixin: - def pre_save(self, obj): - if obj.tags: - _pre_save_new_tags_in_project_tagss_colors(obj) - - super().pre_save(obj) diff --git a/taiga/projects/tagging/serializers.py b/taiga/projects/tagging/serializers.py new file mode 100644 index 00000000..dc25b73a --- /dev/null +++ b/taiga/projects/tagging/serializers.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 as _ + +from taiga.base.api import serializers + +from . import services +from . import fields + +import re + + +class ProjectTagSerializer(serializers.Serializer): + def __init__(self, *args, **kwargs): + # Don't pass the extra project arg + self.project = kwargs.pop("project") + + # Instantiate the superclass normally + super().__init__(*args, **kwargs) + + +class CreateTagSerializer(ProjectTagSerializer): + tag = serializers.CharField() + color = serializers.CharField(required=False) + + def validate_tag(self, attrs, source): + tag = attrs.get(source, None) + if services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag exists.")) + + return attrs + + def validate_color(self, attrs, source): + color = attrs.get(source, None) + if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + raise serializers.ValidationError(_("The color is not a valid HEX color.")) + + return attrs + + +class EditTagTagSerializer(ProjectTagSerializer): + from_tag = serializers.CharField() + to_tag = serializers.CharField(required=False) + color = serializers.CharField(required=False) + + def validate_from_tag(self, attrs, source): + tag = attrs.get(source, None) + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs + + def validate_to_tag(self, attrs, source): + tag = attrs.get(source, None) + if services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag exists yet")) + + return attrs + + def validate_color(self, attrs, source): + color = attrs.get(source, None) + if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + raise serializers.ValidationError(_("The color is not a valid HEX color.")) + + return attrs + + +class DeleteTagSerializer(ProjectTagSerializer): + tag = serializers.CharField() + + def validate_tag(self, attrs, source): + tag = attrs.get(source, None) + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs + + +class MixTagsSerializer(ProjectTagSerializer): + from_tags = fields.TagsField() + to_tag = serializers.CharField() + + def validate_from_tags(self, attrs, source): + tags = attrs.get(source, None) + for tag in tags: + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs + + def validate_to_tag(self, attrs, source): + tag = attrs.get(source, None) + if not services.tag_exist_for_project_elements(self.project, tag): + raise serializers.ValidationError(_("The tag doesn't exist.")) + + return attrs diff --git a/taiga/projects/services/tags.py b/taiga/projects/tagging/services.py similarity index 60% rename from taiga/projects/services/tags.py rename to taiga/projects/tagging/services.py index 010a23fb..30e9f9dc 100644 --- a/taiga/projects/services/tags.py +++ b/taiga/projects/tagging/services.py @@ -23,9 +23,14 @@ def tag_exist_for_project_elements(project, tag): return tag in dict(project.tags_colors).keys() +def create_tags(project, new_tags_colors): + project.tags_colors += [[k, v] for k,v in new_tags_colors.items()] + project.save(update_fields=["tags_colors"]) + + def create_tag(project, tag, color): project.tags_colors.append([tag, color]) - project.save() + project.save(update_fields=["tags_colors"]) def edit_tag(project, from_tag, to_tag=None, color=None): @@ -38,9 +43,17 @@ def edit_tag(project, from_tag, to_tag=None, color=None): if to_tag is not None: color = dict(project.tags_colors)[from_tag] sql = """ - UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; - UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; - UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + UPDATE userstories_userstory + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE tasks_task + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE issues_issue + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; """ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag) cursor = connection.cursor() @@ -50,15 +63,23 @@ def edit_tag(project, from_tag, to_tag=None, color=None): project.tags_colors = list(tags_colors.items()) - project.save() + project.save(update_fields=["tags_colors"]) def rename_tag(project, from_tag, to_tag, color=None): color = color or dict(project.tags_colors)[from_tag] sql = """ - UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; - UPDATE tasks_task SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; - UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id={project_id}; + UPDATE userstories_userstory + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE tasks_task + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE issues_issue + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; """ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color) cursor = connection.cursor() @@ -68,14 +89,22 @@ def rename_tag(project, from_tag, to_tag, color=None): tags_colors.pop(from_tag) tags_colors[to_tag] = color project.tags_colors = list(tags_colors.items()) - project.save() + project.save(update_fields=["tags_colors"]) def delete_tag(project, tag): sql = """ - UPDATE userstories_userstory SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id}; - UPDATE tasks_task SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id}; - UPDATE issues_issue SET tags = array_remove(tags, '{tag}') WHERE project_id={project_id}; + UPDATE userstories_userstory + SET tags = array_remove(tags, '{tag}') + WHERE project_id = {project_id}; + + UPDATE tasks_task + SET tags = array_remove(tags, '{tag}') + WHERE project_id = {project_id}; + + UPDATE issues_issue + SET tags = array_remove(tags, '{tag}') + WHERE project_id = {project_id}; """ sql = sql.format(project_id=project.id, tag=tag) cursor = connection.cursor() @@ -84,7 +113,7 @@ def delete_tag(project, tag): tags_colors = dict(project.tags_colors) del tags_colors[tag] project.tags_colors = list(tags_colors.items()) - project.save() + project.save(update_fields=["tags_colors"]) def mix_tags(project, from_tags, to_tag): diff --git a/taiga/projects/tagging/signals.py b/taiga/projects/tagging/signals.py index 562fcba5..cc94461a 100644 --- a/taiga/projects/tagging/signals.py +++ b/taiga/projects/tagging/signals.py @@ -20,4 +20,3 @@ def tags_normalization(sender, instance, **kwargs): if isinstance(instance.tags, (list, tuple)): instance.tags = list(map(str.lower, instance.tags)) - diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 7b5de5f3..9ebf6dfe 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -29,7 +29,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, TaskStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin -from taiga.projects.tagging.mixins import TaggedResourceMixin +from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 028bfe35..b6693c36 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -36,7 +36,7 @@ from taiga.projects.milestones.models import Milestone from taiga.projects.models import Project, UserStoryStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin -from taiga.projects.tagging.mixins import TaggedResourceMixin +from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models From 93e30ceffa39da42dc302be7826c9fa7a37a8ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 13 Jun 2016 18:02:27 +0200 Subject: [PATCH 052/261] Fix sampledata command to generate tags with and without color --- taiga/projects/management/commands/sample_data.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 937b11d7..23bbf598 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -19,6 +19,7 @@ import random import datetime from os import path +from hashlib import sha1 from django.core.management.base import BaseCommand @@ -256,16 +257,21 @@ class Command(BaseCommand): self.create_wiki_page(project, wiki_link.href) + project.refresh_from_db() + + # Set color for some tags: + for tag in project.tags_colors: + if self.sd.boolean(): + tag[1] = self.generate_color(tag[0]) + # Set a value to total_story_points to show the deadline in the backlog project_stats = get_stats_for_project(project) defined_points = project_stats["defined_points"] project.total_story_points = int(defined_points * self.sd.int(5,12) / 10) - project.refresh_from_db() project.save() self.create_likes(project) - def create_attachment(self, obj, order): attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA) membership = self.sd.db_object_from_queryset(obj.project.memberships @@ -552,3 +558,8 @@ class Command(BaseCommand): obj.add_watcher(user) else: obj.add_watcher(user, notify_level) + + def generate_color(self, tag): + color = sha1(tag.encode("utf-8")).hexdigest()[0:6] + return "#{}".format(color) + From bdd0f0b8334e2ec28883482c0bb9cc4cfcac1f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 13 Jun 2016 18:04:47 +0200 Subject: [PATCH 053/261] Fix to add tags in the same order --- taiga/base/utils/collections.py | 80 +++++++++++++++++++++++++++++++++ taiga/projects/tagging/api.py | 4 +- 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 taiga/base/utils/collections.py diff --git a/taiga/base/utils/collections.py b/taiga/base/utils/collections.py new file mode 100644 index 00000000..c5ca3c59 --- /dev/null +++ b/taiga/base/utils/collections.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import collections + + +class OrderedSet(collections.MutableSet): + # Extract from: + # - https://docs.python.org/3/library/collections.abc.html?highlight=orderedset + # - https://code.activestate.com/recipes/576694/ + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # key --> [key, prev, next] + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): + if not self: + raise KeyError('set is empty') + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py index d93ebe72..c2dbd38a 100644 --- a/taiga/projects/tagging/api.py +++ b/taiga/projects/tagging/api.py @@ -18,6 +18,7 @@ from taiga.base import response from taiga.base.decorators import detail_route +from taiga.base.utils.collections import OrderedSet from . import services from . import serializers @@ -101,11 +102,10 @@ class TaggedResourceMixin: def pre_save(self, obj): if obj.tags: self._pre_save_new_tags_in_project_tagss_colors(obj) - super().pre_save(obj) def _pre_save_new_tags_in_project_tagss_colors(self, obj): - new_obj_tags = set() + new_obj_tags = OrderedSet() new_tags_colors = {} for tag in obj.tags: From 63cc560dabfa6ed205089191907275fff2ba72c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 13 Jun 2016 21:02:56 +0200 Subject: [PATCH 054/261] Change TextArrayField to ArrayField for tags, permisions and more fields. --- requirements.txt | 4 +-- taiga/base/fields.py | 1 - .../migrations/0007_auto_20160614_1201.py | 26 ++++++++++++++ taiga/projects/issues/models.py | 8 ++--- .../migrations/0047_auto_20160614_1201.py | 36 +++++++++++++++++++ taiga/projects/models.py | 24 +++++-------- .../tags.py => projects/tagging/models.py} | 14 ++++++-- .../migrations/0010_auto_20160614_1201.py | 26 ++++++++++++++ taiga/projects/tasks/models.py | 8 ++--- .../migrations/0012_auto_20160614_1201.py | 26 ++++++++++++++ taiga/projects/userstories/models.py | 7 ++-- .../migrations/0021_auto_20160614_1201.py | 21 +++++++++++ taiga/users/models.py | 12 +++---- tests/integration/test_importer_api.py | 2 +- tests/models.py | 26 -------------- 15 files changed, 175 insertions(+), 66 deletions(-) create mode 100644 taiga/projects/issues/migrations/0007_auto_20160614_1201.py create mode 100644 taiga/projects/migrations/0047_auto_20160614_1201.py rename taiga/{base/tags.py => projects/tagging/models.py} (72%) create mode 100644 taiga/projects/tasks/migrations/0010_auto_20160614_1201.py create mode 100644 taiga/projects/userstories/migrations/0012_auto_20160614_1201.py create mode 100644 taiga/users/migrations/0021_auto_20160614_1201.py diff --git a/requirements.txt b/requirements.txt index 181e863f..93d225f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ six==1.10.0 amqp==1.4.9 djmail==0.12.0.post1 django-pgjson==0.3.1 -djorm-pgarray==1.2 +djorm-pgarray==1.2 # Use until Taiga 2.1. Keep compatibility with old migrations django-jinja==2.1.2 jinja2==2.8 pygments==2.0.2 @@ -28,7 +28,7 @@ raven==5.10.2 bleach==1.4.2 django-ipware==1.1.3 premailer==2.9.7 -cssutils==1.0.1 # Compatible with python 3.5 +cssutils==1.0.1 # Compatible with python 3.5 lxml==3.5.0 git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d2014ea pyjwkest==1.1.5 diff --git a/taiga/base/fields.py b/taiga/base/fields.py index 8e95801d..5e5c4b5a 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -18,7 +18,6 @@ from django.forms import widgets from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy from taiga.base.api import serializers diff --git a/taiga/projects/issues/migrations/0007_auto_20160614_1201.py b/taiga/projects/issues/migrations/0007_auto_20160614_1201.py new file mode 100644 index 00000000..5cf43d30 --- /dev/null +++ b/taiga/projects/issues/migrations/0007_auto_20160614_1201.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-14 12:01 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('issues', '0006_remove_issue_watchers'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='external_reference', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'), + ), + migrations.AlterField( + model_name='issue', + name='tags', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'), + ), + ] diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index cd962e08..7c9f6b2e 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -18,17 +18,16 @@ from django.db import models from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.fields import ArrayField from django.conf import settings from django.utils import timezone from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ -from djorm_pgarray.fields import TextArrayField - from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin -from taiga.base.tags import TaggedMixin +from taiga.projects.tagging.models import TaggedMixin class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): @@ -63,7 +62,8 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. default=None, related_name="issues_assigned_to_me", verbose_name=_("assigned to")) attachments = GenericRelation("attachments.Attachment") - external_reference = TextArrayField(default=None, verbose_name=_("external reference")) + external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), + null=True, blank=True, default=[], verbose_name=_("external reference")) _importing = None class Meta: diff --git a/taiga/projects/migrations/0047_auto_20160614_1201.py b/taiga/projects/migrations/0047_auto_20160614_1201.py new file mode 100644 index 00000000..eccd1f46 --- /dev/null +++ b/taiga/projects/migrations/0047_auto_20160614_1201.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-14 12:01 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0046_triggers_to_update_tags_colors'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='anon_permissions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'), + ), + migrations.AlterField( + model_name='project', + name='public_permissions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'), + ), + migrations.AlterField( + model_name='project', + name='tags', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'), + ), + migrations.AlterField( + model_name='project', + name='tags_colors', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='tags colors'), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 4a369c8d..0c6bcc09 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -20,21 +20,22 @@ import itertools import uuid from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models from django.db.models import signals, Q from django.apps import apps from django.conf import settings from django.dispatch import receiver -from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.utils.functional import cached_property from django_pgjson.fields import JsonField -from djorm_pgarray.fields import TextArrayField -from taiga.base.tags import TaggedMixin +from taiga.projects.tagging.models import TaggedMixin +from taiga.projects.tagging.models import TagsColorsdMixin from taiga.base.utils.dicts import dict_sum from taiga.base.utils.files import get_file_path from taiga.base.utils.sequence import arithmetic_progression @@ -141,7 +142,7 @@ class ProjectDefaults(models.Model): abstract = True -class Project(ProjectDefaults, TaggedMixin, models.Model): +class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): name = models.CharField(max_length=250, null=False, blank=False, verbose_name=_("name")) slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, @@ -186,16 +187,12 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): blank=True, default=None, verbose_name=_("creation template")) - anon_permissions = TextArrayField(blank=True, null=True, - default=[], - verbose_name=_("anonymous permissions"), - choices=ANON_PERMISSIONS) - public_permissions = TextArrayField(blank=True, null=True, - default=[], - verbose_name=_("user permissions"), - choices=MEMBERS_PERMISSIONS) is_private = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("is private")) + anon_permissions = ArrayField(models.TextField(null=False, blank=False, choices=ANON_PERMISSIONS), + null=True, blank=True, default=[], verbose_name=_("anonymous permissions")) + public_permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS), + null=True, blank=True, default=[], verbose_name=_("user permissions")) is_featured = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is featured")) @@ -214,9 +211,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): null=True, blank=True, default=None, db_index=True) - tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True, - verbose_name=_("tags colors")) - transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None, verbose_name=_("project transfer token")) diff --git a/taiga/base/tags.py b/taiga/projects/tagging/models.py similarity index 72% rename from taiga/base/tags.py rename to taiga/projects/tagging/models.py index 0e1cd866..970dae40 100644 --- a/taiga/base/tags.py +++ b/taiga/projects/tagging/models.py @@ -18,13 +18,21 @@ # along with this program. If not, see . from django.db import models +from django.contrib.postgres.fields import ArrayField from django.utils.translation import ugettext_lazy as _ -from djorm_pgarray.fields import TextArrayField - class TaggedMixin(models.Model): - tags = TextArrayField(default=None, verbose_name=_("tags")) + tags = ArrayField(models.TextField(), + null=True, blank=True, default=[], verbose_name=_("tags")) + + class Meta: + abstract = True + + +class TagsColorsdMixin(models.Model): + tags_colors = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), + null=True, blank=True, default=[], verbose_name=_("tags colors")) class Meta: abstract = True diff --git a/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py b/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py new file mode 100644 index 00000000..4c3968fa --- /dev/null +++ b/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-14 12:01 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0009_auto_20151104_1131'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='external_reference', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'), + ), + migrations.AlterField( + model_name='task', + name='tags', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'), + ), + ] diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 30406387..18ff5750 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -18,16 +18,15 @@ from django.db import models from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.fields import ArrayField from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from djorm_pgarray.fields import TextArrayField - from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin -from taiga.base.tags import TaggedMixin +from taiga.projects.tagging.models import TaggedMixin class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): @@ -66,7 +65,8 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M attachments = GenericRelation("attachments.Attachment") is_iocaine = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is iocaine")) - external_reference = TextArrayField(default=None, verbose_name=_("external reference")) + external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), + null=True, blank=True, default=[], verbose_name=_("external reference")) _importing = None class Meta: diff --git a/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py b/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py new file mode 100644 index 00000000..1e9830e5 --- /dev/null +++ b/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-14 12:01 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userstories', '0011_userstory_tribe_gig'), + ] + + operations = [ + migrations.AlterField( + model_name='userstory', + name='external_reference', + field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'), + ), + migrations.AlterField( + model_name='userstory', + name='tags', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags'), + ), + ] diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 86332b46..47019ed3 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -18,14 +18,14 @@ from django.db import models from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.fields import ArrayField from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from djorm_pgarray.fields import TextArrayField from picklefield.fields import PickledObjectField -from taiga.base.tags import TaggedMixin +from taiga.projects.tagging.models import TaggedMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin @@ -103,7 +103,8 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod on_delete=models.SET_NULL, related_name="generated_user_stories", verbose_name=_("generated from issue")) - external_reference = TextArrayField(default=None, verbose_name=_("external reference")) + external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), + null=True, blank=True, default=[], verbose_name=_("external reference")) tribe_gig = PickledObjectField(null=True, blank=True, default=None, verbose_name="taiga tribe gig") diff --git a/taiga/users/migrations/0021_auto_20160614_1201.py b/taiga/users/migrations/0021_auto_20160614_1201.py new file mode 100644 index 00000000..a9f1bb98 --- /dev/null +++ b/taiga/users/migrations/0021_auto_20160614_1201.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-14 12:01 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0020_auto_20160525_1229'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='permissions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'), + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index 2d8fcb33..264d1539 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -26,6 +26,7 @@ from django.apps.config import MODELS_MODULE_NAME from django.conf import settings from django.contrib.auth.models import UserManager, AbstractBaseUser from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import ArrayField from django.core import validators from django.core.exceptions import AppRegistryNotReady from django.db import models @@ -34,7 +35,6 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django_pgjson.fields import JsonField -from djorm_pgarray.fields import TextArrayField from taiga.auth.tokens import get_token_for_user from taiga.base.utils.slug import slugify_uniquely @@ -53,8 +53,8 @@ def get_user_model_safe(): registry not being ready yet. Raises LookupError if model isn't found. - Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340 - Ongoing Django issue: https://code.djangoproject.com/ticket/22872 + Based on: https://github.com/django-oscar/django-oscar/blob/1.0/oscar/core/loading.py#L310-L340 + Ongoing Django issue: https://code.djangoproject.com/ticket/22872 """ user_app, user_model = settings.AUTH_USER_MODEL.split('.') @@ -293,10 +293,8 @@ class Role(models.Model): verbose_name=_("name")) slug = models.SlugField(max_length=250, null=False, blank=True, verbose_name=_("slug")) - permissions = TextArrayField(blank=True, null=True, - default=[], - verbose_name=_("permissions"), - choices=MEMBERS_PERMISSIONS) + permissions = ArrayField(models.TextField(null=False, blank=False, choices=MEMBERS_PERMISSIONS), + null=True, blank=True, default=[], verbose_name=_("permissions")) order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order")) # null=True is for make work django 1.7 migrations. project diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 490ba83b..6a2e7883 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -68,7 +68,7 @@ def test_valid_project_import_without_extra_data(client): } response = client.json.post(url, json.dumps(data)) - assert response.status_code == 201 + assert response.status_code == 201, response.data must_empty_children = [ "issues", "user_stories", "us_statuses", "wiki_pages", "priorities", "severities", "milestones", "points", "issue_types", "task_statuses", diff --git a/tests/models.py b/tests/models.py index 9583b8c0..e69de29b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Anler Hernández -# 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.db import models -from taiga.base import tags - - -class TaggedModel(tags.TaggedMixin, models.Model): - class Meta: - app_label = "tests" From a4f2493848209be742671d9328edc2274edc6404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 15 Jun 2016 17:13:00 +0200 Subject: [PATCH 055/261] Issue #3964: Order project templates --- CHANGELOG.md | 1 + .../fixtures/initial_project_templates.json | 2 ++ .../migrations/0048_auto_20160615_1508.py | 24 +++++++++++++++++++ taiga/projects/models.py | 4 +++- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 taiga/projects/migrations/0048_auto_20160615_1508.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c30c98..439d9bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Include created, modified and finished dates for tasks in CSV reports. - Add gravatar url to Users API endpoint. +- ProjectTemplates now are sorted by the attribute 'order'. - Comments: - Now comment owners and project admins can edit existing comments with the history Entry endpoint. - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index 54e73bd0..ea638da4 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -5,6 +5,7 @@ "fields": { "name": "Scrum", "slug": "scrum", + "order": 1, "description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers", "created_date": "2014-04-22T14:48:43.596Z", "modified_date": "2014-07-25T10:02:46.479Z", @@ -32,6 +33,7 @@ "fields": { "name": "Kanban", "slug": "kanban", + "order": 2, "description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.", "created_date": "2014-04-22T14:50:19.738Z", "modified_date": "2014-07-25T13:11:42.754Z", diff --git a/taiga/projects/migrations/0048_auto_20160615_1508.py b/taiga/projects/migrations/0048_auto_20160615_1508.py new file mode 100644 index 00000000..ab8ab0be --- /dev/null +++ b/taiga/projects/migrations/0048_auto_20160615_1508.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-15 15:08 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0047_auto_20160614_1201'), + ] + + operations = [ + migrations.AlterModelOptions( + name='projecttemplate', + options={'ordering': ['order', 'name'], 'verbose_name': 'project template', 'verbose_name_plural': 'project templates'}, + ), + migrations.AddField( + model_name='projecttemplate', + name='order', + field=models.IntegerField(default=10000, verbose_name='user order'), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 0c6bcc09..b0b790d4 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -717,6 +717,8 @@ class ProjectTemplate(models.Model): verbose_name=_("slug"), unique=True) description = models.TextField(null=False, blank=False, verbose_name=_("description")) + order = models.IntegerField(default=10000, null=False, blank=False, + verbose_name=_("user order")) created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), default=timezone.now) @@ -754,7 +756,7 @@ class ProjectTemplate(models.Model): class Meta: verbose_name = "project template" verbose_name_plural = "project templates" - ordering = ["name"] + ordering = ["order", "name"] def __str__(self): return self.name From 117a97f12cfb3ccf84499bb8af40d3f6ee38265b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 17 Jun 2016 10:56:03 +0200 Subject: [PATCH 056/261] Fix external_reference --- taiga/projects/issues/migrations/0007_auto_20160614_1201.py | 2 +- taiga/projects/issues/models.py | 4 ++-- taiga/projects/tasks/migrations/0010_auto_20160614_1201.py | 2 +- taiga/projects/tasks/models.py | 4 ++-- .../userstories/migrations/0012_auto_20160614_1201.py | 2 +- taiga/projects/userstories/models.py | 5 +++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/taiga/projects/issues/migrations/0007_auto_20160614_1201.py b/taiga/projects/issues/migrations/0007_auto_20160614_1201.py index 5cf43d30..f522d46c 100644 --- a/taiga/projects/issues/migrations/0007_auto_20160614_1201.py +++ b/taiga/projects/issues/migrations/0007_auto_20160614_1201.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='issue', name='external_reference', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=False, null=False), blank=True, default=None, null=True, size=None, verbose_name='external reference'), ), migrations.AlterField( model_name='issue', diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 7c9f6b2e..8f9c18a3 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -62,8 +62,8 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. default=None, related_name="issues_assigned_to_me", verbose_name=_("assigned to")) attachments = GenericRelation("attachments.Attachment") - external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), - null=True, blank=True, default=[], verbose_name=_("external reference")) + external_reference = ArrayField(models.TextField(null=False, blank=False), + null=True, blank=True, default=None, verbose_name=_("external reference")) _importing = None class Meta: diff --git a/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py b/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py index 4c3968fa..f269735a 100644 --- a/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py +++ b/taiga/projects/tasks/migrations/0010_auto_20160614_1201.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='task', name='external_reference', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=False, null=False), blank=True, default=None, null=True, size=None, verbose_name='external reference'), ), migrations.AlterField( model_name='task', diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 18ff5750..15f768d4 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -65,8 +65,8 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M attachments = GenericRelation("attachments.Attachment") is_iocaine = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is iocaine")) - external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), - null=True, blank=True, default=[], verbose_name=_("external reference")) + external_reference = ArrayField(models.TextField(null=False, blank=False), + null=True, blank=True, default=None, verbose_name=_("external reference")) _importing = None class Meta: diff --git a/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py b/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py index 1e9830e5..fd0fe25c 100644 --- a/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py +++ b/taiga/projects/userstories/migrations/0012_auto_20160614_1201.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='userstory', name='external_reference', - field=django.contrib.postgres.fields.ArrayField(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=True, null=True), size=2), blank=True, default=[], null=True, size=None, verbose_name='external reference'), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(blank=False, null=False), blank=True, default=None, null=True, size=None, verbose_name='external reference'), ), migrations.AlterField( model_name='userstory', diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index 47019ed3..d774e5b4 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -55,6 +55,7 @@ class RolePoints(models.Model): def project(self): return self.user_story.project + class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, verbose_name=_("ref")) @@ -103,8 +104,8 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod on_delete=models.SET_NULL, related_name="generated_user_stories", verbose_name=_("generated from issue")) - external_reference = ArrayField(ArrayField(models.TextField(null=True, blank=True), size=2), - null=True, blank=True, default=[], verbose_name=_("external reference")) + external_reference = ArrayField(models.TextField(null=False, blank=False), + null=True, blank=True, default=None, verbose_name=_("external reference")) tribe_gig = PickledObjectField(null=True, blank=True, default=None, verbose_name="taiga tribe gig") From 7968c8037660764d9e3405d95587605db8830621 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 15 Jun 2016 08:52:31 +0200 Subject: [PATCH 057/261] Improving API performance for us, task, milestone and issue listing --- CHANGELOG.md | 1 + requirements.txt | 1 + taiga/base/api/serializers.py | 9 ++ taiga/projects/issues/api.py | 1 + taiga/projects/issues/serializers.py | 34 +++++-- taiga/projects/milestones/api.py | 48 ++++++---- taiga/projects/milestones/serializers.py | 32 ++++++- taiga/projects/milestones/utils.py | 58 ++++++++++++ taiga/projects/mixins/serializers.py | 55 ++++++++++++ taiga/projects/notifications/mixins.py | 18 +++- taiga/projects/serializers.py | 2 + taiga/projects/tasks/api.py | 24 +++-- taiga/projects/tasks/serializers.py | 46 ++++++++-- taiga/projects/userstories/api.py | 28 ++++-- taiga/projects/userstories/serializers.py | 100 ++++++++++++++++++--- taiga/projects/userstories/utils.py | 56 ++++++++++++ taiga/projects/votes/mixins/serializers.py | 17 +++- taiga/users/serializers.py | 19 ++++ 18 files changed, 474 insertions(+), 75 deletions(-) create mode 100644 taiga/projects/milestones/utils.py create mode 100644 taiga/projects/userstories/utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 439d9bb8..3068820d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Select a color (or not) to a tag when add it to stories, issues and tasks. ### Misc +- [API] Improve performance of some calls over list. - Lots of small and not so small bugfixes. diff --git a/requirements.txt b/requirements.txt index 93d225f0..a1262fdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,3 +34,4 @@ git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d pyjwkest==1.1.5 python-dateutil==2.4.2 netaddr==0.7.18 +serpy==0.1.1 diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index a9e5f139..7de82458 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -69,6 +69,7 @@ import copy import datetime import inspect import types +import serpy # Note: We do the following so that users of the framework can use this style: # @@ -1220,3 +1221,11 @@ class HyperlinkedModelSerializer(ModelSerializer): "model_name": model_meta.object_name.lower() } return self._default_view_name % format_kwargs + + +class LightSerializer(serpy.Serializer): + def __init__(self, *args, **kwargs): + kwargs.pop("read_only", None) + kwargs.pop("partial", None) + kwargs.pop("files", None) + super().__init__(*args, **kwargs) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index cae23be3..57acfca8 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -65,6 +65,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W filters.WatchersFilter,) filter_fields = ("project", + "project__slug", "status__is_closed") order_by_fields = ("type", diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 83557f20..4243ea31 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -20,18 +20,24 @@ from taiga.base.api import serializers from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.serializers import BasicIssueStatusSerializer from taiga.mdrender.service import render as mdrender +from taiga.projects.mixins.serializers import OwnerExtraInfoMixin +from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin +from taiga.projects.mixins.serializers import StatusExtraInfoMixin +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer +from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.serializers import BasicIssueStatusSerializer from taiga.projects.validators import ProjectExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin +from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin from taiga.users.serializers import UserBasicInfoSerializer from . import models +import serpy class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): @@ -68,11 +74,23 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa return mdrender(obj.project, obj.description) -class IssueListSerializer(IssueSerializer): - class Meta: - model = models.Issue - read_only_fields = ('id', 'ref', 'created_date', 'modified_date') - exclude = ("description", "description_html") +class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, + OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, + serializers.LightSerializer): + id = serpy.Field() + ref = serpy.Field() + severity = serpy.Field(attr="severity_id") + priority = serpy.Field(attr="priority_id") + type = serpy.Field(attr="type_id") + milestone = serpy.Field(attr="milestone_id") + project = serpy.Field(attr="project_id") + created_date = serpy.Field() + modified_date = serpy.Field() + finished_date = serpy.Field() + subject = serpy.Field() + external_reference = serpy.Field() + version = serpy.Field() + watchers = serpy.Field() class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer): diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index f109060b..1520f2c7 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -22,31 +22,46 @@ from django.db.models import Prefetch from taiga.base import filters from taiga.base import response from taiga.base.decorators import detail_route -from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.utils import get_object_or_404 from taiga.base.utils.db import get_object_or_none -from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin -from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset -from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.userstories import utils as userstories_utils from . import serializers from . import models from . import permissions +from . import utils as milestones_utils import datetime class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): - serializer_class = serializers.MilestoneSerializer permission_classes = (permissions.MilestonePermission,) filter_backends = (filters.CanViewMilestonesFilterBackend,) - filter_fields = ("project", "closed") + filter_fields = ( + "project", + "project__slug", + "closed" + ) queryset = models.Milestone.objects.all() + def get_serializer_class(self, *args, **kwargs): + if self.action == "list": + return serializers.MilestoneListSerializer + + return serializers.MilestoneSerializer + def list(self, request, *args, **kwargs): res = super().list(request, *args, **kwargs) self._add_taiga_info_headers() @@ -72,17 +87,17 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, # Userstories prefetching UserStory = apps.get_model("userstories", "UserStory") - us_qs = UserStory.objects.prefetch_related("role_points", - "role_points__points", - "role_points__role") - us_qs = us_qs.select_related("milestone", - "project", - "status", - "owner", - "assigned_to", - "generated_from_issue") + us_qs = UserStory.objects.select_related("milestone", + "project", + "status", + "owner", + "assigned_to", + "generated_from_issue") + us_qs = userstories_utils.attach_total_points(us_qs) + us_qs = userstories_utils.attach_role_points(us_qs) + us_qs = attach_total_voters_to_queryset(us_qs) us_qs = self.attach_watchers_attrs_to_queryset(us_qs) if self.request.user.is_authenticated(): @@ -94,7 +109,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, # Milestones prefetching qs = qs.select_related("project", "owner") qs = self.attach_watchers_attrs_to_queryset(qs) - + qs = milestones_utils.attach_total_points(qs) + qs = milestones_utils.attach_closed_points(qs) qs = qs.order_by("-estimated_start") return qs diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 2a52be47..b3df7c8f 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -21,14 +21,18 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.utils import json from taiga.projects.notifications.mixins import WatchedResourceModelSerializer +from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin -from ..userstories.serializers import UserStoryListSerializer +from taiga.projects.userstories.serializers import UserStoryListSerializer + from . import models +import serpy -class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, ValidateDuplicatedNameInProjectMixin): - user_stories = UserStoryListSerializer(many=True, required=False, read_only=True) + +class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, + ValidateDuplicatedNameInProjectMixin): total_points = serializers.SerializerMethodField("get_total_points") closed_points = serializers.SerializerMethodField("get_closed_points") @@ -41,3 +45,25 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, Val def get_closed_points(self, obj): return sum(obj.closed_points.values()) + + +class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer): + id = serpy.Field() + name = serpy.Field() + slug = serpy.Field() + owner = serpy.Field(attr="owner_id") + project = serpy.Field(attr="project_id") + estimated_start = serpy.Field() + estimated_finish = serpy.Field() + created_date = serpy.Field() + modified_date = serpy.Field() + closed = serpy.Field() + disponibility = serpy.Field() + order = serpy.Field() + watchers = serpy.Field() + user_stories = serpy.MethodField("get_user_stories") + total_points = serializers.Field(source="total_points_attr") + closed_points = serializers.Field(source="closed_points_attr") + + def get_user_stories(self, obj): + return UserStoryListSerializer(obj.user_stories.all(), many=True).data diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py new file mode 100644 index 00000000..a32d7684 --- /dev/null +++ b/taiga/projects/milestones/utils.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + + +def attach_total_points(queryset, as_field="total_points_attr"): + """Attach total of point values to each object of the queryset. + + :param queryset: A Django milestones queryset object. + :param as_field: Attach the points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT SUM(projects_points.value) + FROM userstories_rolepoints + INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id + INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id + WHERE userstories_userstory.milestone_id = {tbl}.id""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_closed_points(queryset, as_field="closed_points_attr"): + """Attach total of closed point values to each object of the queryset. + + :param queryset: A Django milestones queryset object. + :param as_field: Attach the points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT SUM(projects_points.value) + FROM userstories_rolepoints + INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id + INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id + WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index 07a9b683..2d788298 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -17,9 +17,12 @@ # along with this program. If not, see . from taiga.base.api import serializers +from taiga.users.serializers import ListUserBasicInfoSerializer from django.utils.translation import ugettext as _ +import serpy + class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer): def validate_name(self, attrs, source): @@ -39,3 +42,55 @@ class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer): raise serializers.ValidationError(_("Name duplicated for the project")) return attrs + + +class CachedSerializedUsersMixin(serpy.Serializer): + def to_value(self, instance): + self._serialized_users = {} + return super().to_value(instance) + + def get_user_extra_info(self, user): + if user is None: + return None + + serialized_user = self._serialized_users.get(user.id, None) + if serialized_user is None: + serializer_user = ListUserBasicInfoSerializer(user).data + self._serialized_users[user.id] = serializer_user + + return serialized_user + + +class OwnerExtraInfoMixin(CachedSerializedUsersMixin): + owner = serpy.Field(attr="owner_id") + owner_extra_info = serpy.MethodField() + + def get_owner_extra_info(self, obj): + return self.get_user_extra_info(obj.owner) + + +class AssigedToExtraInfoMixin(CachedSerializedUsersMixin): + assigned_to = serpy.Field(attr="assigned_to_id") + assigned_to_extra_info = serpy.MethodField() + + def get_assigned_to_extra_info(self, obj): + return self.get_user_extra_info(obj.assigned_to) + + +class StatusExtraInfoMixin(serpy.Serializer): + status = serpy.Field(attr="status_id") + status_extra_info = serpy.MethodField() + def to_value(self, instance): + self._serialized_status = {} + return super().to_value(instance) + + def get_status_extra_info(self, obj): + serialized_status = self._serialized_status.get(obj.status_id, None) + if serialized_status is None: + serialized_status = { + "name": _(obj.status.name), + "color": obj.status.color + } + self._serialized_status[obj.status_id] = serialized_status + + return serialized_status diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index ee1d59f8..62db374e 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -15,6 +15,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + +import serpy + from functools import partial from operator import is_not @@ -183,10 +186,7 @@ class WatchedModelMixin(object): return frozenset(filter(is_not_none, participants)) -class WatchedResourceModelSerializer(serializers.ModelSerializer): - is_watcher = serializers.SerializerMethodField("get_is_watcher") - total_watchers = serializers.SerializerMethodField("get_total_watchers") - +class BaseWatchedResourceModelSerializer(object): def get_is_watcher(self, obj): if "request" in self.context: user = self.context["request"].user @@ -199,6 +199,16 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer): return getattr(obj, "total_watchers", 0) or 0 +class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serializers.ModelSerializer): + is_watcher = serializers.SerializerMethodField("get_is_watcher") + total_watchers = serializers.SerializerMethodField("get_total_watchers") + + +class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer): + is_watcher = serializers.SerializerMethodField("get_is_watcher") + total_watchers = serializers.SerializerMethodField("get_total_watchers") + + class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): watchers = WatchersField(required=False) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index f388528f..9c185a97 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import serpy + from django.utils.translation import ugettext as _ from django.db.models import Q diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 9ebf6dfe..b7c13ab1 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -44,13 +44,12 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter) retrieve_exclude_filters = (filters.WatchersFilter,) - filter_fields = [ - "user_story", - "milestone", - "project", - "assigned_to", - "status__is_closed" - ] + filter_fields = ["user_story", + "milestone", + "project", + "project__slug", + "assigned_to", + "status__is_closed"] def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: @@ -95,12 +94,11 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa def get_queryset(self): qs = super().get_queryset() qs = self.attach_votes_attrs_to_queryset(qs) - qs = qs.select_related( - "milestone", - "owner", - "assigned_to", - "status", - "project") + qs = qs.select_related("milestone", + "owner", + "assigned_to", + "status", + "project") return self.attach_watchers_attrs_to_queryset(qs) diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index c0c8334a..d7423e66 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -16,13 +16,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_lazy as _ + from taiga.base.api import serializers from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin - from taiga.projects.milestones.validators import SprintExistsValidator +from taiga.projects.mixins.serializers import OwnerExtraInfoMixin +from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin +from taiga.projects.mixins.serializers import StatusExtraInfoMixin from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicTaskStatusSerializerSerializer from taiga.mdrender.service import render as mdrender @@ -30,11 +36,15 @@ from taiga.projects.tagging.fields import TagsAndTagsColorsField from taiga.projects.tasks.validators import TaskExistsValidator from taiga.projects.validators import ProjectExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin +from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin from taiga.users.serializers import UserBasicInfoSerializer +from taiga.users.services import get_photo_or_gravatar_url +from taiga.users.services import get_big_photo_or_gravatar_url from . import models +import serpy class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): @@ -72,11 +82,35 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat return obj.status is not None and obj.status.is_closed -class TaskListSerializer(TaskSerializer): - class Meta: - model = models.Task - read_only_fields = ('id', 'ref', 'created_date', 'modified_date') - exclude = ("description", "description_html") +class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, + OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, + serializers.LightSerializer): + id = serpy.Field() + user_story = serpy.Field(attr="user_story_id") + ref = serpy.Field() + project = serpy.Field(attr="project_id") + milestone = serpy.Field(attr="milestone_id") + milestone_slug = serpy.MethodField("get_milestone_slug") + created_date = serpy.Field() + modified_date = serpy.Field() + finished_date = serpy.Field() + subject = serpy.Field() + us_order = serpy.Field() + taskboard_order = serpy.Field() + is_iocaine = serpy.Field() + external_reference = serpy.Field() + version = serpy.Field() + watchers = serpy.Field() + is_blocked = serpy.Field() + blocked_note = serpy.Field() + tags = serpy.Field() + is_closed = serpy.MethodField() + + def get_milestone_slug(self, obj): + return obj.milestone.slug if obj.milestone else None + + def get_is_closed(self, obj): + return obj.status is not None and obj.status.is_closed class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer): diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index b6693c36..dbe5c433 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -16,8 +16,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from contextlib import closing +from collections import namedtuple + from django.apps import apps -from django.db import transaction +from django.db import transaction, connection +from django.db.models.sql import datastructures + from django.utils.translation import ugettext as _ from django.http import HttpResponse @@ -27,17 +32,23 @@ from taiga.base import response from taiga.base import status from taiga.base.decorators import list_route from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelListViewSet from taiga.base.api.utils import get_object_or_404 from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.services import take_snapshot from taiga.projects.milestones.models import Milestone from taiga.projects.models import Project, UserStoryStatus -from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin +from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin -from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin +from taiga.projects.userstories.models import RolePoints +from taiga.projects.votes.mixins.viewsets import VotedResourceMixin +from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin +from taiga.projects.userstories.utils import attach_total_points +from taiga.projects.userstories.utils import attach_role_points from . import models from . import permissions @@ -63,6 +74,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi filters.TagsFilter, filters.WatchersFilter) filter_fields = ["project", + "project__slug", "milestone", "milestone__isnull", "is_closed", @@ -87,9 +99,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def get_queryset(self): qs = super().get_queryset() - qs = qs.prefetch_related("role_points", - "role_points__points", - "role_points__role") qs = qs.select_related("milestone", "project", "status", @@ -97,7 +106,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "assigned_to", "generated_from_issue") qs = self.attach_votes_attrs_to_queryset(qs) - return self.attach_watchers_attrs_to_queryset(qs) + qs = self.attach_watchers_attrs_to_queryset(qs) + qs = attach_total_points(qs) + qs = attach_role_points(qs) + return qs def pre_conditions_on_save(self, obj): super().pre_conditions_on_save(obj) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index dae58a18..c863c87b 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -16,6 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from collections import ChainMap + +from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_lazy as _ + from taiga.base.api import serializers from taiga.base.api.utils import get_object_or_404 from taiga.base.fields import PickledObjectField @@ -23,21 +28,33 @@ from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.utils import json -from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.models import Project -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.serializers import BasicUserStoryStatusSerializer from taiga.mdrender.service import render as mdrender + +from taiga.projects.milestones.validators import SprintExistsValidator +from taiga.projects.models import Project, UserStoryStatus +from taiga.projects.mixins.serializers import OwnerExtraInfoMixin +from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin +from taiga.projects.mixins.serializers import StatusExtraInfoMixin +from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer +from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.serializers import BasicUserStoryStatusSerializer from taiga.projects.tagging.fields import TagsAndTagsColorsField from taiga.projects.userstories.validators import UserStoryExistsValidator -from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.validators import UserStoryStatusExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin +from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin from taiga.users.serializers import UserBasicInfoSerializer +from taiga.users.serializers import ListUserBasicInfoSerializer +from taiga.users.services import get_photo_or_gravatar_url +from taiga.users.services import get_big_photo_or_gravatar_url from . import models +import serpy + class RolePointsField(serializers.WritableField): def to_native(self, obj): @@ -106,12 +123,68 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, return mdrender(obj.project, obj.description) -class UserStoryListSerializer(UserStorySerializer): - class Meta: - model = models.UserStory - depth = 0 - read_only_fields = ('created_date', 'modified_date') - exclude = ("description", "description_html") +class ListOriginIssueSerializer(serializers.LightSerializer): + id = serpy.Field() + ref = serpy.Field() + subject = serpy.Field() + + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) + + +class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, + OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, serializers.LightSerializer): + + id = serpy.Field() + ref = serpy.Field() + milestone = serpy.Field(attr="milestone_id") + milestone_slug = serpy.MethodField() + milestone_name = serpy.MethodField() + project = serpy.Field(attr="project_id") + is_closed = serpy.Field() + points = serpy.MethodField() + backlog_order = serpy.Field() + sprint_order = serpy.Field() + kanban_order = serpy.Field() + created_date = serpy.Field() + modified_date = serpy.Field() + finish_date = serpy.Field() + subject = serpy.Field() + client_requirement = serpy.Field() + team_requirement = serpy.Field() + generated_from_issue = serpy.Field(attr="generated_from_issue_id") + external_reference = serpy.Field() + tribe_gig = serpy.Field() + version = serpy.Field() + watchers = serpy.Field() + is_blocked = serpy.Field() + blocked_note = serpy.Field() + tags = serpy.Field() + total_points = serpy.Field("total_points_attr") + comment = serpy.MethodField("get_comment") + origin_issue = ListOriginIssueSerializer(attr="generated_from_issue") + + def to_value(self, instance): + self._serialized_status = {} + return super().to_value(instance) + + def get_milestone_slug(self, obj): + return obj.milestone.slug if obj.milestone else None + + def get_milestone_name(self, obj): + return obj.milestone.name if obj.milestone else None + + def get_points(self, obj): + if obj.role_points_attr is None: + return {} + + return dict(ChainMap(*json.loads(obj.role_points_attr))) + + def get_comment(self, obj): + return "" class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): @@ -128,7 +201,8 @@ class NeighborUserStorySerializer(serializers.ModelSerializer): depth = 0 -class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer): +class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, + serializers.Serializer): project_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) bulk_stories = serializers.CharField() diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py new file mode 100644 index 00000000..36a9970d --- /dev/null +++ b/taiga/projects/userstories/utils.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + + +def attach_total_points(queryset, as_field="total_points_attr"): + """Attach total of point values to each object of the queryset. + + :param queryset: A Django user stories queryset object. + :param as_field: Attach the points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT SUM(projects_points.value) + FROM userstories_rolepoints + INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id + WHERE userstories_rolepoints.user_story_id = {tbl}.id""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_role_points(queryset, as_field="role_points_attr"): + """Attach role point as json column to each object of the queryset. + + :param queryset: A Django user stories queryset object. + :param as_field: Attach the role points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(json_build_object(userstories_rolepoints.role_id, + userstories_rolepoints.points_id))::text + FROM userstories_rolepoints + WHERE userstories_rolepoints.user_story_id = {tbl}.id""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py index 1a6faeb2..fc7a988e 100644 --- a/taiga/projects/votes/mixins/serializers.py +++ b/taiga/projects/votes/mixins/serializers.py @@ -16,13 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import serpy + from taiga.base.api import serializers -class VoteResourceSerializerMixin(serializers.ModelSerializer): - is_voter = serializers.SerializerMethodField("get_is_voter") - total_voters = serializers.SerializerMethodField("get_total_voters") - +class BaseVoteResourceSerializerMixin(object): def get_is_voter(self, obj): # The "is_voted" attribute is attached in the get_queryset of the viewset. return getattr(obj, "is_voter", False) or False @@ -30,3 +29,13 @@ class VoteResourceSerializerMixin(serializers.ModelSerializer): def get_total_voters(self, obj): # The "total_voters" attribute is attached in the get_queryset of the viewset. return getattr(obj, "total_voters", 0) or 0 + + +class VoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serializers.ModelSerializer): + is_voter = serializers.SerializerMethodField("get_is_voter") + total_voters = serializers.SerializerMethodField("get_total_voters") + + +class ListVoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serpy.Serializer): + is_voter = serpy.MethodField("get_is_voter") + total_voters = serpy.MethodField("get_total_voters") diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index e35e56cb..98903ec1 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -33,6 +33,7 @@ from .gravatar import get_gravatar_url from collections import namedtuple import re +import serpy ###################################################### @@ -144,6 +145,24 @@ class UserBasicInfoSerializer(UserSerializer): fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id") +class ListUserBasicInfoSerializer(serpy.Serializer): + username = serpy.Field() + full_name_display = serpy.MethodField() + photo = serpy.MethodField() + big_photo = serpy.MethodField() + is_active = serpy.Field() + id = serpy.Field() + + def get_full_name_display(self, obj): + return obj.get_full_name() + + def get_photo(self, obj): + return get_photo_or_gravatar_url(obj) + + def get_big_photo(self, obj): + return get_big_photo_or_gravatar_url(obj) + + class RecoverySerializer(serializers.Serializer): token = serializers.CharField(max_length=200) password = serializers.CharField(min_length=6) From e4f96e605343c237373a1d4fa55d44b464138085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 17 Jun 2016 17:04:39 +0200 Subject: [PATCH 058/261] Fix a typo --- taiga/projects/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/models.py b/taiga/projects/models.py index b0b790d4..83e36e54 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -103,7 +103,7 @@ class Membership(models.Model): class Meta: verbose_name = "membership" - verbose_name_plural = "membershipss" + verbose_name_plural = "memberships" unique_together = ("user", "project",) ordering = ["project", "user__full_name", "user__username", "user__email", "email"] permissions = ( From d59e90885c7acc23865b40c10600fe4ca2336e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 17 Jun 2016 17:10:05 +0200 Subject: [PATCH 059/261] Enhancement: Create the wiki page if not exist when new links are created --- CHANGELOG.md | 1 + taiga/projects/notifications/services.py | 31 +++--- taiga/projects/wiki/api.py | 42 ++++++-- taiga/projects/wiki/permissions.py | 1 + tests/integration/test_wikilinks.py | 118 +++++++++++++++++++++++ 5 files changed, 173 insertions(+), 20 deletions(-) create mode 100644 tests/integration/test_wikilinks.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3068820d..ef53bc7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Include created, modified and finished dates for tasks in CSV reports. - Add gravatar url to Users API endpoint. - ProjectTemplates now are sorted by the attribute 'order'. +- Create enpty wiki pages (if not exist) when a new link is created. - Comments: - Now comment owners and project admins can edit existing comments with the history Entry endpoint. - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 4a26545b..a4128622 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -223,6 +223,7 @@ def send_notifications(obj, *, history): if settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL == 0: send_sync_notifications(notification.id) + @transaction.atomic def send_sync_notifications(notification_id): """ @@ -261,19 +262,21 @@ def send_sync_notifications(notification_id): msg_id = 'taiga-system' now = datetime.datetime.now() - format_args = {"project_slug": notification.project.slug, - "project_name": notification.project.name, - "msg_id": msg_id, - "time": int(now.timestamp()), - "domain": domain} + format_args = { + "project_slug": notification.project.slug, + "project_name": notification.project.name, + "msg_id": msg_id, + "time": int(now.timestamp()), + "domain": domain + } - headers = {"Message-ID": "<{project_slug}/{msg_id}/{time}@{domain}>".format(**format_args), - "In-Reply-To": "<{project_slug}/{msg_id}@{domain}>".format(**format_args), - "References": "<{project_slug}/{msg_id}@{domain}>".format(**format_args), - - "List-ID": 'Taiga/{project_name} '.format(**format_args), - - "Thread-Index": make_ms_thread_index("<{project_slug}/{msg_id}@{domain}>".format(**format_args), now)} + headers = { + "Message-ID": "<{project_slug}/{msg_id}/{time}@{domain}>".format(**format_args), + "In-Reply-To": "<{project_slug}/{msg_id}@{domain}>".format(**format_args), + "References": "<{project_slug}/{msg_id}@{domain}>".format(**format_args), + "List-ID": 'Taiga/{project_name} '.format(**format_args), + "Thread-Index": make_ms_thread_index("<{project_slug}/{msg_id}@{domain}>".format(**format_args), now) + } for user in notification.notify_users.distinct(): context["user"] = user @@ -370,9 +373,11 @@ def get_projects_watched(user_or_id): user = get_user_model().objects.get(id=user_or_id) project_class = apps.get_model("projects", "Project") - project_ids = user.notify_policies.exclude(notify_level=NotifyLevel.none).values_list("project__id", flat=True) + project_ids = (user.notify_policies.exclude(notify_level=NotifyLevel.none) + .values_list("project__id", flat=True)) return project_class.objects.filter(id__in=project_ids) + def add_watcher(obj, user): """Add a watcher to an object. diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index 2ee75b14..bd82a065 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -18,23 +18,27 @@ from django.utils.translation import ugettext as _ -from taiga.base.api.permissions import IsAuthenticated - -from taiga.base import filters from taiga.base import exceptions as exc +from taiga.base import filters from taiga.base import response -from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api import ModelCrudViewSet +from taiga.base.api import ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin +from taiga.base.api.permissions import IsAuthenticated from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route -from taiga.projects.models import Project + from taiga.mdrender.service import render as mdrender -from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.history.services import take_snapshot +from taiga.projects.models import Project +from taiga.projects.notifications.mixins import WatchedResourceMixin +from taiga.projects.notifications.mixins import WatchersViewSetMixin +from taiga.projects.notifications.services import analize_object_for_watchers +from taiga.projects.notifications.services import send_notifications from taiga.projects.occ import OCCResourceMixin - from . import models from . import permissions from . import serializers @@ -99,3 +103,27 @@ class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): permission_classes = (permissions.WikiLinkPermission,) filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ["project"] + + def post_save(self, obj, created=False): + if created: + self._create_wiki_page_when_create_wiki_link_if_not_exist(obj) + super().pre_save(obj) + + def _create_wiki_page_when_create_wiki_link_if_not_exist(self, wiki_link): + try: + self.check_permissions(request, "create_wiki_page", wiki_link) + except exc.PermissionDenied: + pass + else: + # Create the wiki link and the wiki page if not exist. + wiki_page, created = models.WikiPage.objects.get_or_create( + slug=wiki_link.href, + project=wiki_link.project, + defaults={"owner": self.request.user,"last_modifier": self.request.user}) + + if created: + # Creaste the new history entre, sSet watcher for the new wiki page + # and send notifications about the new page created + history = take_snapshot(wiki_page, user=self.request.user) + analize_object_for_watchers(wiki_page, history.comment, history.owner) + send_notifications(wiki_page, history=history) diff --git a/taiga/projects/wiki/permissions.py b/taiga/projects/wiki/permissions.py index 0afea036..5f2311b2 100644 --- a/taiga/projects/wiki/permissions.py +++ b/taiga/projects/wiki/permissions.py @@ -54,3 +54,4 @@ class WikiLinkPermission(TaigaResourcePermission): partial_update_perms = HasProjectPerm('modify_wiki_link') destroy_perms = HasProjectPerm('delete_wiki_link') list_perms = AllowAny() + create_wiki_page_perms = HasProjectPerm('add_wiki_page') diff --git a/tests/integration/test_wikilinks.py b/tests/integration/test_wikilinks.py new file mode 100644 index 00000000..20e185dc --- /dev/null +++ b/tests/integration/test_wikilinks.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse +from django.core import mail + +from taiga.base.utils import json +from taiga.projects.notifications.choices import NotifyLevel + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_create_wiki_link_of_existent_wiki_page_with_permissions(client): + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_wiki_pages', 'view_wiki_link', + 'add_wiki_page', 'add_wiki_link']) + + f.MembershipFactory.create(project=project, user=project.owner, role=role) + project.owner.notify_policies.filter(project=project).update(notify_level=NotifyLevel.all) + + user = f.UserFactory.create() + f.MembershipFactory.create(project=project, user=user, role=role) + + wiki_page = f.WikiPageFactory.create(project=project, owner=user, slug="test", content="test content") + + mail.outbox = [] + + url = reverse("wiki-links-list") + + data = { + "title": "test", + "href": "test", + "project": project.pk, + } + + assert project.wiki_pages.all().count() == 1 + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert len(mail.outbox) == 0 + assert project.wiki_pages.all().count() == 1 + + +def test_create_wiki_link_of_inexistent_wiki_page_with_permissions(client): + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_wiki_pages', 'view_wiki_link', + 'add_wiki_page', 'add_wiki_link']) + + f.MembershipFactory.create(project=project, user=project.owner, role=role) + project.owner.notify_policies.filter(project=project).update(notify_level=NotifyLevel.all) + + user = f.UserFactory.create() + f.MembershipFactory.create(project=project, user=user, role=role) + + mail.outbox = [] + + url = reverse("wiki-links-list") + + data = { + "title": "test", + "href": "test", + "project": project.pk, + } + + assert project.wiki_pages.all().count() == 0 + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert len(mail.outbox) == 1 + assert project.wiki_pages.all().count() == 1 + + +def test_create_wiki_link_of_inexistent_wiki_page_without_permissions(client): + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=['view_wiki_pages', 'view_wiki_link', + 'add_wiki_link']) + + f.MembershipFactory.create(project=project, user=project.owner, role=role) + project.owner.notify_policies.filter(project=project).update(notify_level=NotifyLevel.all) + + user = f.UserFactory.create() + f.MembershipFactory.create(project=project, user=user, role=role) + + mail.outbox = [] + + url = reverse("wiki-links-list") + + data = { + "title": "test", + "href": "test", + "project": project.pk, + } + + assert project.wiki_pages.all().count() == 0 + client.login(user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + assert len(mail.outbox) == 0 + assert project.wiki_pages.all().count() == 0 From 549c022bf8cdc1a32715cb517df4697b03cded3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 21 Jun 2016 09:59:38 +0200 Subject: [PATCH 060/261] Add license disclaimer to some files --- taiga/projects/wiki/api.py | 5 +++-- .../test_application_tokens_resources.py | 18 ++++++++++++++++++ .../test_attachment_resources.py | 18 ++++++++++++++++++ .../test_auth_resources.py | 18 ++++++++++++++++++ .../resources_permissions/test_feedback.py | 18 ++++++++++++++++++ .../test_history_resources.py | 18 ++++++++++++++++++ .../test_issues_resources.py | 18 ++++++++++++++++++ .../test_milestones_resources.py | 18 ++++++++++++++++++ .../test_modules_resources.py | 18 ++++++++++++++++++ .../test_projects_choices_resources.py | 18 ++++++++++++++++++ .../test_projects_resource.py | 18 ++++++++++++++++++ .../test_resolver_resources.py | 18 ++++++++++++++++++ .../test_search_resources.py | 18 ++++++++++++++++++ .../test_storage_resources.py | 18 ++++++++++++++++++ .../test_tasks_resources.py | 18 ++++++++++++++++++ .../test_timelines_resources.py | 18 ++++++++++++++++++ .../test_users_resources.py | 18 ++++++++++++++++++ .../test_userstories_resources.py | 18 ++++++++++++++++++ .../test_webhooks_resources.py | 18 ++++++++++++++++++ .../test_wiki_resources.py | 18 ++++++++++++++++++ tests/integration/test_application_tokens.py | 18 ++++++++++++++++++ tests/integration/test_attachments.py | 18 ++++++++++++++++++ tests/integration/test_feedback.py | 18 ++++++++++++++++++ tests/integration/test_hooks_bitbucket.py | 18 ++++++++++++++++++ tests/integration/test_hooks_github.py | 18 ++++++++++++++++++ tests/integration/test_hooks_gitlab.py | 18 ++++++++++++++++++ tests/integration/test_issues.py | 18 ++++++++++++++++++ tests/integration/test_issues_tags.py | 18 ++++++++++++++++++ tests/integration/test_memberships.py | 18 ++++++++++++++++++ tests/integration/test_models.py | 18 ++++++++++++++++++ tests/integration/test_permissions.py | 18 ++++++++++++++++++ tests/integration/test_projects.py | 18 ++++++++++++++++++ tests/integration/test_stats.py | 18 ++++++++++++++++++ tests/integration/test_tasks.py | 18 ++++++++++++++++++ tests/integration/test_tasks_tags.py | 18 ++++++++++++++++++ tests/integration/test_users.py | 18 ++++++++++++++++++ tests/integration/test_userstories.py | 18 ++++++++++++++++++ tests/integration/test_userstories_tags.py | 18 ++++++++++++++++++ tests/unit/test_serializer_mixins.py | 18 ++++++++++++++++++ 39 files changed, 687 insertions(+), 2 deletions(-) diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index bd82a065..807d86c6 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -106,13 +106,14 @@ class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): def post_save(self, obj, created=False): if created: - self._create_wiki_page_when_create_wiki_link_if_not_exist(obj) + self._create_wiki_page_when_create_wiki_link_if_not_exist(self.request, obj) super().pre_save(obj) - def _create_wiki_page_when_create_wiki_link_if_not_exist(self, wiki_link): + def _create_wiki_page_when_create_wiki_link_if_not_exist(self, request, wiki_link): try: self.check_permissions(request, "create_wiki_page", wiki_link) except exc.PermissionDenied: + # Create only the wiki link because the user doesn't have permission. pass else: # Create the wiki link and the wiki page if not exist. diff --git a/tests/integration/resources_permissions/test_application_tokens_resources.py b/tests/integration/resources_permissions/test_application_tokens_resources.py index 5f6d27e7..10a880b5 100644 --- a/tests/integration/resources_permissions/test_application_tokens_resources.py +++ b/tests/integration/resources_permissions/test_application_tokens_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py index 3fdd990a..c30fb854 100644 --- a/tests/integration/resources_permissions/test_attachment_resources.py +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from django.core.files.uploadedfile import SimpleUploadedFile from django.test.client import MULTIPART_CONTENT diff --git a/tests/integration/resources_permissions/test_auth_resources.py b/tests/integration/resources_permissions/test_auth_resources.py index 18684d4e..4604936f 100644 --- a/tests/integration/resources_permissions/test_auth_resources.py +++ b/tests/integration/resources_permissions/test_auth_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_feedback.py b/tests/integration/resources_permissions/test_feedback.py index 650f6b07..752c1e71 100644 --- a/tests/integration/resources_permissions/test_feedback.py +++ b/tests/integration/resources_permissions/test_feedback.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from tests import factories as f diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index c466a9db..f9df4a0d 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from django.utils import timezone diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 922b21e8..d9b391f8 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index d3567ffb..a1a06172 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py index e9403787..34ad7369 100644 --- a/tests/integration/resources_permissions/test_modules_resources.py +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index 2e95f731..77a0b754 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 78c55324..77e79542 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from django.apps import apps diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index 137983e9..b8c43d04 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index bc8c9ac4..7ce732be 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_storage_resources.py b/tests/integration/resources_permissions/test_storage_resources.py index ebc1eb8a..8d2ddf3b 100644 --- a/tests/integration/resources_permissions/test_storage_resources.py +++ b/tests/integration/resources_permissions/test_storage_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 3e5f1824..5eaf5243 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_timelines_resources.py b/tests/integration/resources_permissions/test_timelines_resources.py index 1a63db0c..a6930a29 100644 --- a/tests/integration/resources_permissions/test_timelines_resources.py +++ b/tests/integration/resources_permissions/test_timelines_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS diff --git a/tests/integration/resources_permissions/test_users_resources.py b/tests/integration/resources_permissions/test_users_resources.py index 394dd1f8..b15d9cde 100644 --- a/tests/integration/resources_permissions/test_users_resources.py +++ b/tests/integration/resources_permissions/test_users_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 tempfile import NamedTemporaryFile from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 55a97c90..c9f95a31 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import uuid from django.core.urlresolvers import reverse diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py index dd10f04c..afc3597d 100644 --- a/tests/integration/resources_permissions/test_webhooks_resources.py +++ b/tests/integration/resources_permissions/test_webhooks_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/resources_permissions/test_wiki_resources.py b/tests/integration/resources_permissions/test_wiki_resources.py index 0026e263..f4cdbc12 100644 --- a/tests/integration/resources_permissions/test_wiki_resources.py +++ b/tests/integration/resources_permissions/test_wiki_resources.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.base.utils import json diff --git a/tests/integration/test_application_tokens.py b/tests/integration/test_application_tokens.py index b34076b2..4ec5c0cd 100644 --- a/tests/integration/test_application_tokens.py +++ b/tests/integration/test_application_tokens.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from taiga.external_apps import encryption diff --git a/tests/integration/test_attachments.py b/tests/integration/test_attachments.py index 94e8e3cf..06a49441 100644 --- a/tests/integration/test_attachments.py +++ b/tests/integration/test_attachments.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from django.core.urlresolvers import reverse diff --git a/tests/integration/test_feedback.py b/tests/integration/test_feedback.py index 777c65c8..82de1139 100644 --- a/tests/integration/test_feedback.py +++ b/tests/integration/test_feedback.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from tests import factories as f diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 4b278f38..0f74078e 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest import urllib diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index f472f479..4cdeec76 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from unittest import mock diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index bf86a80c..f90f74b0 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from unittest import mock diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 16fc156f..a14b2db4 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import uuid import csv diff --git a/tests/integration/test_issues_tags.py b/tests/integration/test_issues_tags.py index 5a38bab0..c86dc7aa 100644 --- a/tests/integration/test_issues_tags.py +++ b/tests/integration/test_issues_tags.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 unittest import mock from collections import OrderedDict diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index 580378f5..d5a8a5c3 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 unittest import mock from django.core.urlresolvers import reverse diff --git a/tests/integration/test_models.py b/tests/integration/test_models.py index b0926901..6c6229b1 100644 --- a/tests/integration/test_models.py +++ b/tests/integration/test_models.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from .. import factories as f diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py index d40e6afc..bc39384f 100644 --- a/tests/integration/test_permissions.py +++ b/tests/integration/test_permissions.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from taiga.permissions import services, choices diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 29d57c50..5cadc245 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.core.urlresolvers import reverse from django.conf import settings from django.core.files import File diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py index e85f22ab..5dfdd3d9 100644 --- a/tests/integration/test_stats.py +++ b/tests/integration/test_stats.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from .. import factories as f diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 712fa07e..fcecee3b 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import uuid import csv diff --git a/tests/integration/test_tasks_tags.py b/tests/integration/test_tasks_tags.py index 67a27c0d..08bf434d 100644 --- a/tests/integration/test_tasks_tags.py +++ b/tests/integration/test_tasks_tags.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 unittest import mock from collections import OrderedDict diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 2c25b29d..90ed5599 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from tempfile import NamedTemporaryFile diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 2a8499a0..bc3c5560 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import copy import uuid import csv diff --git a/tests/integration/test_userstories_tags.py b/tests/integration/test_userstories_tags.py index 1313dc91..c2244f19 100644 --- a/tests/integration/test_userstories_tags.py +++ b/tests/integration/test_userstories_tags.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 unittest import mock from collections import OrderedDict diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index 26d7ea41..349a912c 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + import pytest from .. import factories as f From b5202636e14ed394b0daae4e9107ea45c998f098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 Jun 2016 18:06:12 +0200 Subject: [PATCH 061/261] Improve performance on exports --- taiga/export_import/serializers.py | 50 ++++++++++++++++++++------ taiga/export_import/services/render.py | 9 +++-- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 43acb5af..16877ea7 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -50,6 +50,28 @@ from taiga.projects.notifications import services as notifications_services from taiga.projects.votes import services as votes_service from taiga.projects.history import services as history_service +_cache_user_by_pk = {} +_cache_user_by_email = {} +_custom_tasks_attributes_cache = {} +_custom_issues_attributes_cache = {} +_custom_userstories_attributes_cache = {} + +def cached_get_user_by_pk(pk): + if pk not in _cache_user_by_pk: + try: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + except Exception: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + return _cache_user_by_pk[pk] + +def cached_get_user_by_email(email): + if email not in _cache_user_by_email: + try: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + except Exception: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + return _cache_user_by_email[email] + class FileField(serializers.WritableField): read_only = False @@ -128,7 +150,7 @@ class UserRelatedField(RelatedNoneSafeField): def from_native(self, data): try: - return users_models.User.objects.get(email=data) + return cached_get_user_by_email(data) except users_models.User.DoesNotExist: return None @@ -138,14 +160,14 @@ class UserPkField(serializers.RelatedField): def to_native(self, obj): try: - user = users_models.User.objects.get(pk=obj) + user = cached_get_user_by_pk(obj) return user.email except users_models.User.DoesNotExist: return None def from_native(self, data): try: - user = users_models.User.objects.get(email=data) + user = cached_get_user_by_email(data) return user.pk except users_models.User.DoesNotExist: return None @@ -185,7 +207,7 @@ class HistoryUserField(JsonField): if obj is None or obj == {}: return [] try: - user = users_models.User.objects.get(pk=obj['pk']) + user = cached_get_user_by_pk(obj['pk']) except users_models.User.DoesNotExist: user = None return (UserRelatedField().to_native(user), obj['name']) @@ -420,7 +442,7 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): try: values = obj.custom_attributes_values.attributes_values - custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name') + custom_attributes = self.custom_attributes_queryset(obj.project) return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) except ObjectDoesNotExist: @@ -550,7 +572,9 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE exclude = ('id', 'project') def custom_attributes_queryset(self, project): - return project.taskcustomattributes.all() + if project.id not in _custom_tasks_attributes_cache: + _custom_tasks_attributes_cache[project.id] = list(project.taskcustomattributes.all().values('id', 'name')) + return _custom_tasks_attributes_cache[project.id] class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, @@ -568,7 +592,9 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His exclude = ('id', 'project', 'points', 'tasks') def custom_attributes_queryset(self, project): - return project.userstorycustomattributes.all() + if project.id not in _custom_userstories_attributes_cache: + _custom_userstories_attributes_cache[project.id] = list(project.userstorycustomattributes.all().values('id', 'name')) + return _custom_userstories_attributes_cache[project.id] class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, @@ -591,7 +617,9 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History return [x.email for x in votes_service.get_voters(obj)] def custom_attributes_queryset(self, project): - return project.issuecustomattributes.all() + if project.id not in _custom_issues_attributes_cache: + _custom_issues_attributes_cache[project.id] = list(project.issuecustomattributes.all().values('id', 'name')) + return _custom_issues_attributes_cache[project.id] class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, @@ -618,17 +646,17 @@ class TimelineDataField(serializers.WritableField): def to_native(self, data): new_data = copy.deepcopy(data) try: - user = users_models.User.objects.get(pk=new_data["user"]["id"]) + user = cached_get_user_by_pk(new_data["user"]["id"]) new_data["user"]["email"] = user.email del new_data["user"]["id"] - except users_models.User.DoesNotExist: + except Exception: pass return new_data def from_native(self, data): new_data = copy.deepcopy(data) try: - user = users_models.User.objects.get(email=new_data["user"]["email"]) + user = cached_get_user_by_email(new_data["user"]["email"]) new_data["user"]["id"] = user.id del new_data["user"]["email"] except users_models.User.DoesNotExist: diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py index cc4f8edf..19015878 100644 --- a/taiga/export_import/services/render.py +++ b/taiga/export_import/services/render.py @@ -50,6 +50,12 @@ def render_project(project, outfile, chunk_size = 8190): # These four "special" fields hava attachments so we use them in a special way if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]: value = get_component(project, field_name) + if field_name != "wiki_pages": + value = value.select_related('owner', 'status', 'milestone', 'project', 'assigned_to', 'custom_attributes_values') + if field_name == "issues": + value = value.select_related('severity', 'priority', 'type') + value = value.prefetch_related('history_entry', 'attachments') + outfile.write('"{}": [\n'.format(field_name)) attachments_field = field.fields.pop("attachments", None) @@ -101,9 +107,8 @@ def render_project(project, outfile, chunk_size = 8190): outfile.write(']}') outfile.flush() - gc.collect() + gc.collect() outfile.write(']') - else: value = field.field_to_native(project, field_name) outfile.write('"{}": {}'.format(field_name, json.dumps(value))) From 1336f5c8e92ddd5f5a0d756293da0ccad23f685c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 15 Jun 2016 20:06:46 +0200 Subject: [PATCH 062/261] Add gzip support to exports --- taiga/export_import/api.py | 21 +++++++--- .../management/commands/dump_project.py | 19 +++++++-- taiga/export_import/services/render.py | 32 +++++++------- taiga/export_import/tasks.py | 27 ++++++++---- tests/integration/test_exporter_api.py | 42 ++++++++++++++++++- tests/unit/test_export.py | 4 +- 6 files changed, 109 insertions(+), 36 deletions(-) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index bf5cf1a9..4e6f87e0 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -18,6 +18,7 @@ import codecs import uuid +import gzip from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ @@ -64,16 +65,24 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet) project = get_object_or_404(self.get_queryset(), pk=pk) self.check_permissions(request, 'export_project', project) + dump_format = request.QUERY_PARAMS.get("dump_format", None) + if settings.CELERY_ENABLED: - task = tasks.dump_project.delay(request.user, project) - tasks.delete_project_dump.apply_async((project.pk, project.slug, task.id), + task = tasks.dump_project.delay(request.user, project, dump_format) + tasks.delete_project_dump.apply_async((project.pk, project.slug, task.id, dump_format), countdown=settings.EXPORTS_TTL) return response.Accepted({"export_id": task.id}) - path = "exports/{}/{}-{}.json".format(project.pk, project.slug, uuid.uuid4().hex) - storage_path = default_storage.path(path) - with default_storage.open(storage_path, mode="w") as outfile: - services.render_project(project, outfile) + if dump_format == "gzip": + path = "exports/{}/{}-{}.json.gz".format(project.pk, project.slug, uuid.uuid4().hex) + storage_path = default_storage.path(path) + with default_storage.open(storage_path, mode="wb") as outfile: + services.render_project(project, gzip.GzipFile(fileobj=outfile)) + else: + path = "exports/{}/{}-{}.json".format(project.pk, project.slug, uuid.uuid4().hex) + storage_path = default_storage.path(path) + with default_storage.open(storage_path, mode="wb") as outfile: + services.render_project(project, outfile) response_data = { "url": default_storage.url(path) diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py index 3b3ceaf6..3c6996f3 100644 --- a/taiga/export_import/management/commands/dump_project.py +++ b/taiga/export_import/management/commands/dump_project.py @@ -22,6 +22,7 @@ from taiga.projects.models import Project from taiga.export_import.services import render_project import os +import gzip class Command(BaseCommand): @@ -39,6 +40,13 @@ class Command(BaseCommand): metavar="DIR", help="Directory to save the json files. ('./' by default)") + parser.add_argument("-f", "--format", + action="store", + dest="format", + default="plain", + metavar="[plain|gzip]", + help="Format to the output file plain json or gzipped json. ('plain' by default)") + def handle(self, *args, **options): dst_dir = options["dst_dir"] @@ -56,8 +64,13 @@ class Command(BaseCommand): except Project.DoesNotExist: raise CommandError("Project '{}' does not exist".format(project_slug)) - dst_file = os.path.join(dst_dir, "{}.json".format(project_slug)) - with open(dst_file, "w") as f: - render_project(project, f) + if options["format"] == "gzip": + dst_file = os.path.join(dst_dir, "{}.json.gz".format(project_slug)) + with gzip.GzipFile(dst_file, "wb") as f: + render_project(project, f) + else: + dst_file = os.path.join(dst_dir, "{}.json".format(project_slug)) + with open(dst_file, "wb") as f: + render_project(project, f) print("-> Generate dump of project '{}' in '{}'".format(project.name, dst_file)) diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py index 19015878..923647a7 100644 --- a/taiga/export_import/services/render.py +++ b/taiga/export_import/services/render.py @@ -34,13 +34,13 @@ from .. import serializers def render_project(project, outfile, chunk_size = 8190): serializer = serializers.ProjectExportSerializer(project) - outfile.write('{\n') + outfile.write(b'{\n') first_field = True for field_name in serializer.fields.keys(): # Avoid writing "," in the last element if not first_field: - outfile.write(",\n") + outfile.write(b",\n") else: first_field = False @@ -56,7 +56,7 @@ def render_project(project, outfile, chunk_size = 8190): value = value.select_related('severity', 'priority', 'type') value = value.prefetch_related('history_entry', 'attachments') - outfile.write('"{}": [\n'.format(field_name)) + outfile.write('"{}": [\n'.format(field_name).encode()) attachments_field = field.fields.pop("attachments", None) if attachments_field: @@ -66,20 +66,20 @@ def render_project(project, outfile, chunk_size = 8190): for item in value.iterator(): # Avoid writing "," in the last element if not first_item: - outfile.write(",\n") + outfile.write(b",\n") else: first_item = False dumped_value = json.dumps(field.to_native(item)) writing_value = dumped_value[:-1]+ ',\n "attachments": [\n' - outfile.write(writing_value) + outfile.write(writing_value.encode()) first_attachment = True for attachment in item.attachments.iterator(): # Avoid writing "," in the last element if not first_attachment: - outfile.write(",\n") + outfile.write(b",\n") else: first_attachment = False @@ -88,7 +88,7 @@ def render_project(project, outfile, chunk_size = 8190): attached_file_serializer = attachment_serializer.fields.pop("attached_file") dumped_value = json.dumps(attachment_serializer.data) dumped_value = dumped_value[:-1] + ',\n "attached_file":{\n "data":"' - outfile.write(dumped_value) + outfile.write(dumped_value.encode()) # We write the attached_files by chunks so the memory used is not increased attachment_file = attachment.attached_file @@ -99,32 +99,32 @@ def render_project(project, outfile, chunk_size = 8190): if not bin_data: break - b64_data = base64.b64encode(bin_data).decode('utf-8') + b64_data = base64.b64encode(bin_data) outfile.write(b64_data) outfile.write('", \n "name":"{}"}}\n}}'.format( - os.path.basename(attachment_file.name))) + os.path.basename(attachment_file.name)).encode()) - outfile.write(']}') + outfile.write(b']}') outfile.flush() gc.collect() - outfile.write(']') + outfile.write(b']') else: value = field.field_to_native(project, field_name) - outfile.write('"{}": {}'.format(field_name, json.dumps(value))) + outfile.write('"{}": {}'.format(field_name, json.dumps(value)).encode()) # Generate the timeline - outfile.write(',\n"timeline": [\n') + outfile.write(b',\n"timeline": [\n') first_timeline = True for timeline_item in get_project_timeline(project).iterator(): # Avoid writing "," in the last element if not first_timeline: - outfile.write(",\n") + outfile.write(b",\n") else: first_timeline = False dumped_value = json.dumps(serializers.TimelineExportSerializer(timeline_item).data) - outfile.write(dumped_value) + outfile.write(dumped_value.encode()) - outfile.write(']}\n') + outfile.write(b']}\n') diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index 8ba61645..aa75c257 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -19,6 +19,7 @@ import datetime import logging import sys +import gzip from django.core.files.storage import default_storage from django.core.files.base import ContentFile @@ -41,14 +42,20 @@ import resource @app.task(bind=True) -def dump_project(self, user, project): - path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id) - storage_path = default_storage.path(path) - +def dump_project(self, user, project, dump_format): try: + if dump_format == "gzip": + path = "exports/{}/{}-{}.json.gz".format(project.pk, project.slug, self.request.id) + storage_path = default_storage.path(path) + with default_storage.open(storage_path, mode="wb") as outfile: + services.render_project(project, gzip.GzipFile(fileobj=outfile)) + else: + path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id) + storage_path = default_storage.path(path) + with default_storage.open(storage_path, mode="wb") as outfile: + services.render_project(project, outfile) + url = default_storage.url(path) - with default_storage.open(storage_path, mode="w") as outfile: - services.render_project(project, outfile) except Exception: # Error @@ -75,8 +82,12 @@ def dump_project(self, user, project): @app.task -def delete_project_dump(project_id, project_slug, task_id): - default_storage.delete("exports/{}/{}-{}.json".format(project_id, project_slug, task_id)) +def delete_project_dump(project_id, project_slug, task_id, dump_format): + if dump_format == "gzip": + path = "exports/{}/{}-{}.json.gz".format(project_id, project_slug, task_id) + else: + path = "exports/{}/{}-{}.json".format(project_id, project_slug, task_id) + default_storage.delete(path) ADMIN_ERROR_LOAD_PROJECT_DUMP_MESSAGE = _(""" diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py index c8727ae8..5ea4dd0e 100644 --- a/tests/integration/test_exporter_api.py +++ b/tests/integration/test_exporter_api.py @@ -53,6 +53,24 @@ def test_valid_project_export_with_celery_disabled(client, settings): assert response.status_code == 200 response_data = response.data assert "url" in response_data + assert response_data["url"].endswith(".json") + + +def test_valid_project_export_with_celery_disabled_and_gzip(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory(project=project, user=user, is_admin=True) + client.login(user) + + url = reverse("exporter-detail", args=[project.pk]) + + response = client.get(url+"?dump_format=gzip", content_type="application/json") + assert response.status_code == 200 + response_data = response.data + assert "url" in response_data + assert response_data["url"].endswith(".gz") def test_valid_project_export_with_celery_enabled(client, settings): @@ -72,7 +90,29 @@ def test_valid_project_export_with_celery_enabled(client, settings): response_data = response.data assert "export_id" in response_data - args = (project.id, project.slug, response_data["export_id"],) + args = (project.id, project.slug, response_data["export_id"], None) + kwargs = {"countdown": settings.EXPORTS_TTL} + delete_project_dump_mock.apply_async.assert_called_once_with(args, **kwargs) + + +def test_valid_project_export_with_celery_enabled_and_gzip(client, settings): + settings.CELERY_ENABLED = True + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory(project=project, user=user, is_admin=True) + client.login(user) + + url = reverse("exporter-detail", args=[project.pk]) + + #delete_project_dump task should have been launched + with mock.patch('taiga.export_import.tasks.delete_project_dump') as delete_project_dump_mock: + response = client.get(url+"?dump_format=gzip", content_type="application/json") + assert response.status_code == 202 + response_data = response.data + assert "export_id" in response_data + + args = (project.id, project.slug, response_data["export_id"], "gzip") kwargs = {"countdown": settings.EXPORTS_TTL} delete_project_dump_mock.apply_async.assert_called_once_with(args, **kwargs) diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py index 546814a8..a8ce775f 100644 --- a/tests/unit/test_export.py +++ b/tests/unit/test_export.py @@ -28,7 +28,7 @@ pytestmark = pytest.mark.django_db def test_export_issue_finish_date(client): issue = f.IssueFactory.create(finished_date="2014-10-22") - output = io.StringIO() + output = io.BytesIO() render_project(issue.project, output) project_data = json.loads(output.getvalue()) finish_date = project_data["issues"][0]["finished_date"] @@ -37,7 +37,7 @@ def test_export_issue_finish_date(client): def test_export_user_story_finish_date(client): user_story = f.UserStoryFactory.create(finish_date="2014-10-22") - output = io.StringIO() + output = io.BytesIO() render_project(user_story.project, output) project_data = json.loads(output.getvalue()) finish_date = project_data["user_stories"][0]["finish_date"] From 1fa5a12d061346240e30fb59a4389797476cab3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 17 Jun 2016 09:33:12 +0200 Subject: [PATCH 063/261] Reestructure serializers module --- taiga/export_import/serializers/__init__.py | 45 ++ taiga/export_import/serializers/cache.py | 42 ++ taiga/export_import/serializers/fields.py | 250 ++++++++++++ taiga/export_import/serializers/mixins.py | 141 +++++++ .../{ => serializers}/serializers.py | 384 +----------------- 5 files changed, 495 insertions(+), 367 deletions(-) create mode 100644 taiga/export_import/serializers/__init__.py create mode 100644 taiga/export_import/serializers/cache.py create mode 100644 taiga/export_import/serializers/fields.py create mode 100644 taiga/export_import/serializers/mixins.py rename taiga/export_import/{ => serializers}/serializers.py (52%) diff --git a/taiga/export_import/serializers/__init__.py b/taiga/export_import/serializers/__init__.py new file mode 100644 index 00000000..5d793a87 --- /dev/null +++ b/taiga/export_import/serializers/__init__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 .serializers import PointsExportSerializer +from .serializers import UserStoryStatusExportSerializer +from .serializers import TaskStatusExportSerializer +from .serializers import IssueStatusExportSerializer +from .serializers import PriorityExportSerializer +from .serializers import SeverityExportSerializer +from .serializers import IssueTypeExportSerializer +from .serializers import RoleExportSerializer +from .serializers import UserStoryCustomAttributeExportSerializer +from .serializers import TaskCustomAttributeExportSerializer +from .serializers import IssueCustomAttributeExportSerializer +from .serializers import BaseCustomAttributesValuesExportSerializer +from .serializers import UserStoryCustomAttributesValuesExportSerializer +from .serializers import TaskCustomAttributesValuesExportSerializer +from .serializers import IssueCustomAttributesValuesExportSerializer +from .serializers import MembershipExportSerializer +from .serializers import RolePointsExportSerializer +from .serializers import MilestoneExportSerializer +from .serializers import TaskExportSerializer +from .serializers import UserStoryExportSerializer +from .serializers import IssueExportSerializer +from .serializers import WikiPageExportSerializer +from .serializers import WikiLinkExportSerializer +from .serializers import TimelineExportSerializer +from .serializers import ProjectExportSerializer +from .mixins import AttachmentExportSerializer +from .mixins import HistoryExportSerializer diff --git a/taiga/export_import/serializers/cache.py b/taiga/export_import/serializers/cache.py new file mode 100644 index 00000000..c4eb5bfa --- /dev/null +++ b/taiga/export_import/serializers/cache.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.users import models as users_models + +_cache_user_by_pk = {} +_cache_user_by_email = {} +_custom_tasks_attributes_cache = {} +_custom_issues_attributes_cache = {} +_custom_userstories_attributes_cache = {} + + +def cached_get_user_by_pk(pk): + if pk not in _cache_user_by_pk: + try: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + except Exception: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + return _cache_user_by_pk[pk] + +def cached_get_user_by_email(email): + if email not in _cache_user_by_email: + try: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + except Exception: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + return _cache_user_by_email[email] diff --git a/taiga/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py new file mode 100644 index 00000000..f2ca8841 --- /dev/null +++ b/taiga/export_import/serializers/fields.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import base64 +import os +import copy +from collections import OrderedDict + +from django.core.files.base import ContentFile +from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext as _ + +from taiga.base.api import serializers +from taiga.base.fields import JsonField +from taiga.mdrender.service import render as mdrender +from taiga.users import models as users_models + +from .cache import cached_get_user_by_email, cached_get_user_by_pk + + +class FileField(serializers.WritableField): + read_only = False + + def to_native(self, obj): + if not obj: + return None + + data = base64.b64encode(obj.read()).decode('utf-8') + + return OrderedDict([ + ("data", data), + ("name", os.path.basename(obj.name)), + ]) + + def from_native(self, data): + if not data: + return None + + decoded_data = b'' + # The original file was encoded by chunks but we don't really know its + # length or if it was multiple of 3 so we must iterate over all those chunks + # decoding them one by one + for decoding_chunk in data['data'].split("="): + # When encoding to base64 3 bytes are transformed into 4 bytes and + # the extra space of the block is filled with = + # We must ensure that the decoding chunk has a length multiple of 4 so + # we restore the stripped '='s adding appending them until the chunk has + # a length multiple of 4 + decoding_chunk += "=" * (-len(decoding_chunk) % 4) + decoded_data += base64.b64decode(decoding_chunk+"=") + + return ContentFile(decoded_data, name=data['name']) + + +class RelatedNoneSafeField(serializers.RelatedField): + 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() + + key = self.source or field_name + if value in self.null_values: + if self.required: + raise ValidationError(self.error_messages['required']) + into[key] = None + elif self.many: + into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] + else: + into[key] = self.from_native(value) + + +class UserRelatedField(RelatedNoneSafeField): + read_only = False + + def to_native(self, obj): + if obj: + return obj.email + return None + + def from_native(self, data): + try: + return cached_get_user_by_email(data) + except users_models.User.DoesNotExist: + return None + + +class UserPkField(serializers.RelatedField): + read_only = False + + def to_native(self, obj): + try: + user = cached_get_user_by_pk(obj) + return user.email + except users_models.User.DoesNotExist: + return None + + def from_native(self, data): + try: + user = cached_get_user_by_email(data) + return user.pk + except users_models.User.DoesNotExist: + return None + + +class CommentField(serializers.WritableField): + read_only = False + + def field_from_native(self, data, files, field_name, into): + super().field_from_native(data, files, field_name, into) + into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) + + +class ProjectRelatedField(serializers.RelatedField): + read_only = False + null_values = (None, "") + + def __init__(self, slug_field, *args, **kwargs): + self.slug_field = slug_field + super().__init__(*args, **kwargs) + + def to_native(self, obj): + if obj: + return getattr(obj, self.slug_field) + return None + + def from_native(self, data): + try: + 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))) + + +class HistoryUserField(JsonField): + def to_native(self, obj): + if obj is None or obj == {}: + return [] + try: + user = cached_get_user_by_pk(obj['pk']) + except users_models.User.DoesNotExist: + user = None + return (UserRelatedField().to_native(user), obj['name']) + + def from_native(self, data): + if data is None: + return {} + + if len(data) < 2: + return {} + + user = UserRelatedField().from_native(data[0]) + + if user: + pk = user.pk + else: + pk = None + + return {"pk": pk, "name": data[1]} + + +class HistoryValuesField(JsonField): + def to_native(self, obj): + if obj is None: + return [] + if "users" in obj: + obj['users'] = list(map(UserPkField().to_native, obj['users'])) + return obj + + def from_native(self, data): + if data is None: + return [] + if "users" in data: + data['users'] = list(map(UserPkField().from_native, data['users'])) + return data + + +class HistoryDiffField(JsonField): + def to_native(self, obj): + if obj is None: + return [] + + if "assigned_to" in obj: + obj['assigned_to'] = list(map(UserPkField().to_native, obj['assigned_to'])) + + return obj + + def from_native(self, data): + if data is None: + return [] + + if "assigned_to" in data: + data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) + return data + + +class TimelineDataField(serializers.WritableField): + read_only = False + + def to_native(self, data): + new_data = copy.deepcopy(data) + try: + user = cached_get_user_by_pk(new_data["user"]["id"]) + new_data["user"]["email"] = user.email + del new_data["user"]["id"] + except Exception: + pass + return new_data + + def from_native(self, data): + new_data = copy.deepcopy(data) + try: + user = cached_get_user_by_email(new_data["user"]["email"]) + new_data["user"]["id"] = user.id + del new_data["user"]["email"] + except users_models.User.DoesNotExist: + pass + + return new_data diff --git a/taiga/export_import/serializers/mixins.py b/taiga/export_import/serializers/mixins.py new file mode 100644 index 00000000..007649a2 --- /dev/null +++ b/taiga/export_import/serializers/mixins.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.contenttypes.models import ContentType + +from taiga.base.api import serializers +from taiga.projects.history import models as history_models +from taiga.projects.attachments import models as attachments_models +from taiga.projects.notifications import services as notifications_services +from taiga.projects.history import services as history_service + +from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField, + JsonField, HistoryValuesField, CommentField, FileField) + + +class HistoryExportSerializer(serializers.ModelSerializer): + user = HistoryUserField() + diff = HistoryDiffField(required=False) + snapshot = JsonField(required=False) + values = HistoryValuesField(required=False) + comment = CommentField(required=False) + delete_comment_date = serializers.DateTimeField(required=False) + delete_comment_user = HistoryUserField(required=False) + + class Meta: + model = history_models.HistoryEntry + exclude = ("id", "comment_html", "key") + + +class HistoryExportSerializerMixin(serializers.ModelSerializer): + history = serializers.SerializerMethodField("get_history") + + def get_history(self, obj): + history_qs = history_service.get_history_queryset_by_model_instance(obj, + types=(history_models.HistoryType.change, history_models.HistoryType.create,)) + + return HistoryExportSerializer(history_qs, many=True).data + + +class AttachmentExportSerializer(serializers.ModelSerializer): + owner = UserRelatedField(required=False) + attached_file = FileField() + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = attachments_models.Attachment + exclude = ('id', 'content_type', 'object_id', 'project') + + +class AttachmentExportSerializerMixin(serializers.ModelSerializer): + attachments = serializers.SerializerMethodField("get_attachments") + + def get_attachments(self, obj): + content_type = ContentType.objects.get_for_model(obj.__class__) + attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, + content_type=content_type) + return AttachmentExportSerializer(attachments_qs, many=True).data + + +class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): + custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") + + def custom_attributes_queryset(self, project): + raise NotImplementedError() + + def get_custom_attributes_values(self, obj): + def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values): + ret = {} + for attr in custom_attributes: + value = values.get(str(attr["id"]), None) + if value is not None: + ret[attr["name"]] = value + + return ret + + try: + values = obj.custom_attributes_values.attributes_values + custom_attributes = self.custom_attributes_queryset(obj.project) + + return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) + except ObjectDoesNotExist: + return None + + +class WatcheableObjectModelSerializerMixin(serializers.ModelSerializer): + watchers = UserRelatedField(many=True, required=False) + + def __init__(self, *args, **kwargs): + self._watchers_field = self.base_fields.pop("watchers", None) + super(WatcheableObjectModelSerializerMixin, self).__init__(*args, **kwargs) + + """ + watchers is not a field from the model so we need to do some magic to make it work like a normal field + It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances + """ + + def restore_object(self, attrs, instance=None): + watcher_field = self.fields.pop("watchers", None) + instance = super(WatcheableObjectModelSerializerMixin, self).restore_object(attrs, instance) + self._watchers = self.init_data.get("watchers", []) + return instance + + def save_watchers(self): + new_watcher_emails = set(self._watchers) + old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) + adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) + removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) + + User = get_user_model() + adding_users = User.objects.filter(email__in=adding_watcher_emails) + removing_users = User.objects.filter(email__in=removing_watcher_emails) + + for user in adding_users: + notifications_services.add_watcher(self.object, user) + + for user in removing_users: + notifications_services.remove_watcher(self.object, user) + + self.object.watchers = [user.email for user in self.object.get_watchers()] + + def to_native(self, obj): + ret = super(WatcheableObjectModelSerializerMixin, self).to_native(obj) + ret["watchers"] = [user.email for user in obj.get_watchers()] + return ret diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers/serializers.py similarity index 52% rename from taiga/export_import/serializers.py rename to taiga/export_import/serializers/serializers.py index 16877ea7..e7a2af76 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -16,25 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import base64 import copy -import os -from collections import OrderedDict -from django.apps import apps -from django.contrib.auth import get_user_model -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 taiga.base.api import serializers from taiga.base.fields import JsonField, PgArrayField -from taiga.mdrender.service import render as mdrender from taiga.projects import models as projects_models from taiga.projects.custom_attributes import models as custom_attributes_models from taiga.projects.userstories import models as userstories_models @@ -46,308 +35,19 @@ from taiga.projects.history import models as history_models from taiga.projects.attachments import models as attachments_models from taiga.timeline import models as timeline_models from taiga.users import models as users_models -from taiga.projects.notifications import services as notifications_services from taiga.projects.votes import services as votes_service -from taiga.projects.history import services as history_service -_cache_user_by_pk = {} -_cache_user_by_email = {} -_custom_tasks_attributes_cache = {} -_custom_issues_attributes_cache = {} -_custom_userstories_attributes_cache = {} - -def cached_get_user_by_pk(pk): - if pk not in _cache_user_by_pk: - try: - _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) - except Exception: - _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) - return _cache_user_by_pk[pk] - -def cached_get_user_by_email(email): - if email not in _cache_user_by_email: - try: - _cache_user_by_email[email] = users_models.User.objects.get(email=email) - except Exception: - _cache_user_by_email[email] = users_models.User.objects.get(email=email) - return _cache_user_by_email[email] - - -class FileField(serializers.WritableField): - read_only = False - - def to_native(self, obj): - if not obj: - return None - - data = base64.b64encode(obj.read()).decode('utf-8') - - return OrderedDict([ - ("data", data), - ("name", os.path.basename(obj.name)), - ]) - - def from_native(self, data): - if not data: - return None - - decoded_data = b'' - # The original file was encoded by chunks but we don't really know its - # length or if it was multiple of 3 so we must iterate over all those chunks - # decoding them one by one - for decoding_chunk in data['data'].split("="): - # When encoding to base64 3 bytes are transformed into 4 bytes and - # the extra space of the block is filled with = - # We must ensure that the decoding chunk has a length multiple of 4 so - # we restore the stripped '='s adding appending them until the chunk has - # a length multiple of 4 - decoding_chunk += "=" * (-len(decoding_chunk) % 4) - decoded_data += base64.b64decode(decoding_chunk+"=") - - return ContentFile(decoded_data, name=data['name']) - - -class RelatedNoneSafeField(serializers.RelatedField): - 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() - - key = self.source or field_name - if value in self.null_values: - if self.required: - raise ValidationError(self.error_messages['required']) - into[key] = None - elif self.many: - into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] - else: - into[key] = self.from_native(value) - - -class UserRelatedField(RelatedNoneSafeField): - read_only = False - - def to_native(self, obj): - if obj: - return obj.email - return None - - def from_native(self, data): - try: - return cached_get_user_by_email(data) - except users_models.User.DoesNotExist: - return None - - -class UserPkField(serializers.RelatedField): - read_only = False - - def to_native(self, obj): - try: - user = cached_get_user_by_pk(obj) - return user.email - except users_models.User.DoesNotExist: - return None - - def from_native(self, data): - try: - user = cached_get_user_by_email(data) - return user.pk - except users_models.User.DoesNotExist: - return None - - -class CommentField(serializers.WritableField): - read_only = False - - def field_from_native(self, data, files, field_name, into): - super().field_from_native(data, files, field_name, into) - into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) - - -class ProjectRelatedField(serializers.RelatedField): - read_only = False - null_values = (None, "") - - def __init__(self, slug_field, *args, **kwargs): - self.slug_field = slug_field - super().__init__(*args, **kwargs) - - def to_native(self, obj): - if obj: - return getattr(obj, self.slug_field) - return None - - def from_native(self, data): - try: - 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))) - - -class HistoryUserField(JsonField): - def to_native(self, obj): - if obj is None or obj == {}: - return [] - try: - user = cached_get_user_by_pk(obj['pk']) - except users_models.User.DoesNotExist: - user = None - return (UserRelatedField().to_native(user), obj['name']) - - def from_native(self, data): - if data is None: - return {} - - if len(data) < 2: - return {} - - user = UserRelatedField().from_native(data[0]) - - if user: - pk = user.pk - else: - pk = None - - return {"pk": pk, "name": data[1]} - - -class HistoryValuesField(JsonField): - def to_native(self, obj): - if obj is None: - return [] - if "users" in obj: - obj['users'] = list(map(UserPkField().to_native, obj['users'])) - return obj - - def from_native(self, data): - if data is None: - return [] - if "users" in data: - data['users'] = list(map(UserPkField().from_native, data['users'])) - return data - - -class HistoryDiffField(JsonField): - def to_native(self, obj): - if obj is None: - return [] - - if "assigned_to" in obj: - obj['assigned_to'] = list(map(UserPkField().to_native, obj['assigned_to'])) - - return obj - - def from_native(self, data): - if data is None: - return [] - - if "assigned_to" in data: - data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) - return data - - -class WatcheableObjectModelSerializer(serializers.ModelSerializer): - watchers = UserRelatedField(many=True, required=False) - - def __init__(self, *args, **kwargs): - self._watchers_field = self.base_fields.pop("watchers", None) - super(WatcheableObjectModelSerializer, self).__init__(*args, **kwargs) - - """ - watchers is not a field from the model so we need to do some magic to make it work like a normal field - It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances - """ - - def restore_object(self, attrs, instance=None): - watcher_field = self.fields.pop("watchers", None) - instance = super(WatcheableObjectModelSerializer, self).restore_object(attrs, instance) - self._watchers = self.init_data.get("watchers", []) - return instance - - def save_watchers(self): - new_watcher_emails = set(self._watchers) - old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) - adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) - removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) - - User = get_user_model() - adding_users = User.objects.filter(email__in=adding_watcher_emails) - removing_users = User.objects.filter(email__in=removing_watcher_emails) - - for user in adding_users: - notifications_services.add_watcher(self.object, user) - - for user in removing_users: - notifications_services.remove_watcher(self.object, user) - - self.object.watchers = [user.email for user in self.object.get_watchers()] - - def to_native(self, obj): - ret = super(WatcheableObjectModelSerializer, self).to_native(obj) - ret["watchers"] = [user.email for user in obj.get_watchers()] - return ret - - -class HistoryExportSerializer(serializers.ModelSerializer): - user = HistoryUserField() - diff = HistoryDiffField(required=False) - snapshot = JsonField(required=False) - values = HistoryValuesField(required=False) - comment = CommentField(required=False) - delete_comment_date = serializers.DateTimeField(required=False) - delete_comment_user = HistoryUserField(required=False) - - class Meta: - model = history_models.HistoryEntry - exclude = ("id", "comment_html", "key") - - -class HistoryExportSerializerMixin(serializers.ModelSerializer): - history = serializers.SerializerMethodField("get_history") - - def get_history(self, obj): - history_qs = history_service.get_history_queryset_by_model_instance(obj, - types=(history_models.HistoryType.change, history_models.HistoryType.create,)) - - return HistoryExportSerializer(history_qs, many=True).data - - -class AttachmentExportSerializer(serializers.ModelSerializer): - owner = UserRelatedField(required=False) - attached_file = FileField() - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = attachments_models.Attachment - exclude = ('id', 'content_type', 'object_id', 'project') - - -class AttachmentExportSerializerMixin(serializers.ModelSerializer): - attachments = serializers.SerializerMethodField("get_attachments") - - def get_attachments(self, obj): - content_type = ContentType.objects.get_for_model(obj.__class__) - attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, - content_type=content_type) - return AttachmentExportSerializer(attachments_qs, many=True).data +from .fields import (FileField, RelatedNoneSafeField, UserRelatedField, + UserPkField, CommentField, ProjectRelatedField, + HistoryUserField, HistoryValuesField, HistoryDiffField, + TimelineDataField) +from .mixins import (HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + CustomAttributesValuesExportSerializerMixin, + WatcheableObjectModelSerializerMixin) +from .cache import (_custom_tasks_attributes_cache, + _custom_userstories_attributes_cache, + _custom_issues_attributes_cache) class PointsExportSerializer(serializers.ModelSerializer): @@ -424,31 +124,6 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') -class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): - custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") - - def custom_attributes_queryset(self, project): - raise NotImplementedError() - - def get_custom_attributes_values(self, obj): - def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values): - ret = {} - for attr in custom_attributes: - value = values.get(str(attr["id"]), None) - if value is not None: - ret[attr["name"]] = value - - return ret - - try: - values = obj.custom_attributes_values.attributes_values - custom_attributes = self.custom_attributes_queryset(obj.project) - - return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) - except ObjectDoesNotExist: - return None - - class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): attributes_values = JsonField(source="attributes_values",required=True) _custom_attribute_model = None @@ -530,7 +205,7 @@ class RolePointsExportSerializer(serializers.ModelSerializer): exclude = ('id', 'user_story') -class MilestoneExportSerializer(WatcheableObjectModelSerializer): +class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) modified_date = serializers.DateTimeField(required=False) estimated_start = serializers.DateField(required=False) @@ -559,7 +234,7 @@ class MilestoneExportSerializer(WatcheableObjectModelSerializer): class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") user_story = ProjectRelatedField(slug_field="ref", required=False) @@ -578,7 +253,7 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): role_points = RolePointsExportSerializer(many=True, required=False) owner = UserRelatedField(required=False) assigned_to = UserRelatedField(required=False) @@ -598,7 +273,7 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializer): + AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) status = ProjectRelatedField(slug_field="name") assigned_to = UserRelatedField(required=False) @@ -623,7 +298,7 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - WatcheableObjectModelSerializer): + WatcheableObjectModelSerializerMixin): owner = UserRelatedField(required=False) last_modifier = UserRelatedField(required=False) modified_date = serializers.DateTimeField(required=False) @@ -640,31 +315,6 @@ class WikiLinkExportSerializer(serializers.ModelSerializer): -class TimelineDataField(serializers.WritableField): - read_only = False - - def to_native(self, data): - new_data = copy.deepcopy(data) - try: - user = cached_get_user_by_pk(new_data["user"]["id"]) - new_data["user"]["email"] = user.email - del new_data["user"]["id"] - except Exception: - pass - return new_data - - def from_native(self, data): - new_data = copy.deepcopy(data) - try: - user = cached_get_user_by_email(new_data["user"]["email"]) - new_data["user"]["id"] = user.id - del new_data["user"]["email"] - except users_models.User.DoesNotExist: - pass - - return new_data - - class TimelineExportSerializer(serializers.ModelSerializer): data = TimelineDataField() class Meta: @@ -672,7 +322,7 @@ class TimelineExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project', 'namespace', 'object_id') -class ProjectExportSerializer(WatcheableObjectModelSerializer): +class ProjectExportSerializer(WatcheableObjectModelSerializerMixin): logo = FileField(required=False) anon_permissions = PgArrayField(required=False) public_permissions = PgArrayField(required=False) From 44d46ad47b87e9d11cd0ac70e4abd7c10da9527a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 17 Jun 2016 11:41:58 +0200 Subject: [PATCH 064/261] Fixing timeline exportation --- taiga/export_import/serializers/fields.py | 16 ++++++++++++++++ taiga/export_import/serializers/serializers.py | 5 +++-- taiga/export_import/services/store.py | 3 ++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/taiga/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py index f2ca8841..64c01436 100644 --- a/taiga/export_import/serializers/fields.py +++ b/taiga/export_import/serializers/fields.py @@ -25,6 +25,7 @@ from django.core.files.base import ContentFile from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType from taiga.base.api import serializers from taiga.base.fields import JsonField @@ -68,6 +69,21 @@ class FileField(serializers.WritableField): return ContentFile(decoded_data, name=data['name']) +class ContentTypeField(serializers.RelatedField): + read_only = False + + def to_native(self, obj): + if obj: + return [obj.app_label, obj.model] + return None + + def from_native(self, data): + try: + return ContentType.objects.get_by_natural_key(*data) + except Exception: + return None + + class RelatedNoneSafeField(serializers.RelatedField): def field_from_native(self, data, files, field_name, into): if self.read_only: diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py index e7a2af76..7cf46cba 100644 --- a/taiga/export_import/serializers/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -40,7 +40,7 @@ from taiga.projects.votes import services as votes_service from .fields import (FileField, RelatedNoneSafeField, UserRelatedField, UserPkField, CommentField, ProjectRelatedField, HistoryUserField, HistoryValuesField, HistoryDiffField, - TimelineDataField) + TimelineDataField, ContentTypeField) from .mixins import (HistoryExportSerializerMixin, AttachmentExportSerializerMixin, CustomAttributesValuesExportSerializerMixin, @@ -317,9 +317,10 @@ class WikiLinkExportSerializer(serializers.ModelSerializer): class TimelineExportSerializer(serializers.ModelSerializer): data = TimelineDataField() + data_content_type = ContentTypeField() class Meta: model = timeline_models.Timeline - exclude = ('id', 'project', 'namespace', 'object_id') + exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') class ProjectExportSerializer(WatcheableObjectModelSerializerMixin): diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index c7888ce4..5d71c445 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -577,6 +577,7 @@ def _store_timeline_entry(project, timeline): serialized.object.project = project serialized.object.namespace = build_project_namespace(project) serialized.object.object_id = project.id + serialized.object.content_type = ContentType.objects.get_for_model(project.__class__) serialized.object._importing = True serialized.save() return serialized @@ -725,7 +726,7 @@ def store_project_from_dict(data, owner=None): except err.TaigaImportError: # reraise known inport errors raise - except: + except Exception: # reise unknown errors as import error raise err.TaigaImportError(_("unexpected error importing project"), project) From 773ab631064a155983fe173a5d45af336472aa3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 20 Jun 2016 12:24:34 +0200 Subject: [PATCH 065/261] Adding dump_project_async command --- .../management/commands/dump_project_async.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 taiga/export_import/management/commands/dump_project_async.py diff --git a/taiga/export_import/management/commands/dump_project_async.py b/taiga/export_import/management/commands/dump_project_async.py new file mode 100644 index 00000000..d48a0c19 --- /dev/null +++ b/taiga/export_import/management/commands/dump_project_async.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.core.management.base import BaseCommand, CommandError +from django.db.models import Q +from django.conf import settings + +from taiga.projects.models import Project +from taiga.users.models import User +from taiga.permissions.services import is_project_admin +from taiga.export_import import tasks + + +class Command(BaseCommand): + help = "Export projects to a json file" + + def add_arguments(self, parser): + parser.add_argument("project_slugs", + nargs="+", + help="") + + parser.add_argument("-u", "--user", + action="store", + dest="user", + default="./", + metavar="DIR", + required=True, + help="Dump as user by email or username.") + + parser.add_argument("-f", "--format", + action="store", + dest="format", + default="plain", + metavar="[plain|gzip]", + help="Format to the output file plain json or gzipped json. ('plain' by default)") + + def handle(self, *args, **options): + username_or_email = options["user"] + dump_format = options["format"] + project_slugs = options["project_slugs"] + + try: + user = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email)) + except Exception: + raise CommandError("Error loading user".format(username_or_email)) + + for project_slug in project_slugs: + try: + project = Project.objects.get(slug=project_slug) + except Project.DoesNotExist: + raise CommandError("Project '{}' does not exist".format(project_slug)) + + if not is_project_admin(user, project): + self.stderr.write(self.style.ERROR( + "ERROR: Not sending task because user '{}' doesn't have permissions to export '{}' project".format( + username_or_email, + project_slug + ) + )) + continue + + task = tasks.dump_project.delay(user, project, dump_format) + tasks.delete_project_dump.apply_async( + (project.pk, project.slug, task.id, dump_format), + countdown=settings.EXPORTS_TTL + ) + print("-> Sent task for dump of project '{}' as user {}".format(project.name, username_or_email)) From 55d1b042b2812734b1a3c80dd03b9d4503c7a491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 21 Jun 2016 15:02:42 +0200 Subject: [PATCH 066/261] BugFix: use plain format dump by default on exports --- taiga/export_import/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 4e6f87e0..a8a1dddd 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -65,7 +65,7 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet) project = get_object_or_404(self.get_queryset(), pk=pk) self.check_permissions(request, 'export_project', project) - dump_format = request.QUERY_PARAMS.get("dump_format", None) + dump_format = request.QUERY_PARAMS.get("dump_format", "plain") if settings.CELERY_ENABLED: task = tasks.dump_project.delay(request.user, project, dump_format) From bde6f855cedcc0919261b669a9ec599149b8abfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 21 Jun 2016 15:57:12 +0200 Subject: [PATCH 067/261] Fix export test --- tests/integration/test_exporter_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py index 5ea4dd0e..c71fbbb5 100644 --- a/tests/integration/test_exporter_api.py +++ b/tests/integration/test_exporter_api.py @@ -90,7 +90,7 @@ def test_valid_project_export_with_celery_enabled(client, settings): response_data = response.data assert "export_id" in response_data - args = (project.id, project.slug, response_data["export_id"], None) + args = (project.id, project.slug, response_data["export_id"], "plain") kwargs = {"countdown": settings.EXPORTS_TTL} delete_project_dump_mock.apply_async.assert_called_once_with(args, **kwargs) From 2cb4ee6e6c0b96e3d2a9a30da64ab78b1d21e1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 22 Jun 2016 09:20:09 +0200 Subject: [PATCH 068/261] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef53bc7b..d043cbc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog # -## 2.2.0 ??? (unreleased) +## 3.0.0 ??? (unreleased) ### Features - Include created, modified and finished dates for tasks in CSV reports. From 4827df0058fe83922650f4da3ed99f2a3d596f15 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 3 Jun 2016 08:13:38 +0200 Subject: [PATCH 069/261] Updating CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d043cbc5..c4bdf62a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ - New API endpoints over projects to create, rename, edit, delete and mix tags. - Tag color assignation is not automatic. - Select a color (or not) to a tag when add it to stories, issues and tasks. +- Now comment owners and project admins can edit existing comments with the history Entry endpoint. +- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. +- Include created, modified and finished dates for tasks in CSV reports +- User stories and tasks listing API call support extra params to include more data (tasks and attachemnts and attachments, respectively) + ### Misc - [API] Improve performance of some calls over list. From 34446d8289791a9c11ee9a209474c1e05bae361e Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 2 Jun 2016 14:34:44 +0200 Subject: [PATCH 070/261] Adding tasks and attachments to userstory listing API calls and attachments to task listing API call --- taiga/base/filters.py | 6 +-- .../migrations/0006_auto_20160617_1233.py | 19 ++++++++ taiga/projects/attachments/models.py | 1 + taiga/projects/attachments/serializers.py | 32 ++++++++++++++ taiga/projects/attachments/utils.py | 44 +++++++++++++++++++ taiga/projects/issues/serializers.py | 11 ++--- taiga/projects/milestones/serializers.py | 16 ++++++- taiga/projects/mixins/serializers.py | 9 ++-- taiga/projects/tasks/api.py | 20 ++++++--- taiga/projects/tasks/serializers.py | 14 ++++-- taiga/projects/userstories/api.py | 12 +++++ taiga/projects/userstories/serializers.py | 39 +++++++++++----- taiga/projects/userstories/utils.py | 32 +++++++++++++- tests/integration/test_tasks.py | 21 +++++++++ tests/integration/test_userstories.py | 42 ++++++++++++++++++ 15 files changed, 280 insertions(+), 38 deletions(-) create mode 100644 taiga/projects/attachments/migrations/0006_auto_20160617_1233.py create mode 100644 taiga/projects/attachments/utils.py diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 1cd19e64..e26e7911 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -152,7 +152,7 @@ class PermissionBasedFilterBackend(FilterBackend): else: qs = qs.filter(project__anon_permissions__contains=[self.permission]) - return super().filter_queryset(request, qs.distinct(), view) + return super().filter_queryset(request, qs, view) class CanViewProjectFilterBackend(PermissionBasedFilterBackend): @@ -268,7 +268,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend): qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission]) - return qs.distinct() + return qs ##################################################################### @@ -307,7 +307,7 @@ class IsProjectAdminFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend else: queryset = queryset.filter(project_id__in=project_ids) - return super().filter_queryset(request, queryset.distinct(), view) + return super().filter_queryset(request, queryset, view) class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdminFilterBackend): diff --git a/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py b/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py new file mode 100644 index 00000000..ee291a9f --- /dev/null +++ b/taiga/projects/attachments/migrations/0006_auto_20160617_1233.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-17 12:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('attachments', '0005_attachment_sha1'), + ] + + operations = [ + migrations.AlterIndexTogether( + name='attachment', + index_together=set([('content_type', 'object_id')]), + ), + ] diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index 8bbbee16..a5110a4b 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -70,6 +70,7 @@ class Attachment(models.Model): permissions = ( ("view_attachment", "Can view attachment"), ) + index_together = [("content_type", "object_id")] def __init__(self, *args, **kwargs): super(Attachment, self).__init__(*args, **kwargs) diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 904498a9..6c5ee05b 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -16,11 +16,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.conf import settings + from taiga.base.api import serializers +from taiga.base.utils.thumbnails import get_thumbnail_url from . import services from . import models +import json +import serpy + class AttachmentSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField("get_url") @@ -37,5 +43,31 @@ class AttachmentSerializer(serializers.ModelSerializer): def get_url(self, obj): return obj.attached_file.url + def get_thumbnail_card_url(self, obj): return services.get_card_image_thumbnail_url(obj) + + +class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer): + """ + Assumptions: + - The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information + about the related elements, otherwise it will be empty + - The method attach_basic_attachments has been used to include the necessary + json data about the attachments in the "attachments_attr" column + """ + attachments = serpy.MethodField() + + def get_attachments(self, obj): + include_attachments = getattr(obj, "include_attachments", False) + + if include_attachments: + assert hasattr(obj, "attachments_attr"), "instance must have a attachments_attr attribute" + + if not include_attachments or obj.attachments_attr is None: + return [] + + for at in obj.attachments_attr: + at["thumbnail_card_url"] = get_thumbnail_url(at["attached_file"], settings.THN_ATTACHMENT_CARD) + + return obj.attachments_attr diff --git a/taiga/projects/attachments/utils.py b/taiga/projects/attachments/utils.py new file mode 100644 index 00000000..33e36c44 --- /dev/null +++ b/taiga/projects/attachments/utils.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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.apps import apps + +def attach_basic_attachments(queryset, as_field="attachments_attr"): + """Attach basic attachments info as json column to each object of the queryset. + + :param queryset: A Django user stories queryset object. + :param as_field: Attach the role points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + + model = queryset.model + type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) + + sql = """SELECT json_agg(row_to_json(t)) + FROM( + SELECT + attachments_attachment.id, + attachments_attachment.attached_file + FROM attachments_attachment + WHERE attachments_attachment.object_id = {tbl}.id AND attachments_attachment.content_type_id = {type_id}) t""" + + sql = sql.format(tbl=model._meta.db_table, type_id=type.id) + queryset = queryset.extra(select={as_field: sql}) + return queryset diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 4243ea31..099171a1 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -21,9 +21,9 @@ from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender -from taiga.projects.mixins.serializers import OwnerExtraInfoMixin -from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin -from taiga.projects.mixins.serializers import StatusExtraInfoMixin +from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator @@ -39,6 +39,7 @@ from . import models import serpy + class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): tags = TagsAndTagsColorsField(default=[], required=False) @@ -75,8 +76,8 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, - serializers.LightSerializer): + ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, + ListStatusExtraInfoSerializerMixin, serializers.LightSerializer): id = serpy.Field() ref = serpy.Field() severity = serpy.Field(attr="severity_id") diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index b3df7c8f..724126fd 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -35,6 +35,7 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, ValidateDuplicatedNameInProjectMixin): total_points = serializers.SerializerMethodField("get_total_points") closed_points = serializers.SerializerMethodField("get_closed_points") + user_stories = serializers.SerializerMethodField("get_user_stories") class Meta: model = models.Milestone @@ -46,6 +47,9 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, def get_closed_points(self, obj): return sum(obj.closed_points.values()) + def get_user_stories(self, obj): + return UserStoryListSerializer(obj.user_stories.all(), many=True).data + class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer): id = serpy.Field() @@ -62,8 +66,16 @@ class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.Li order = serpy.Field() watchers = serpy.Field() user_stories = serpy.MethodField("get_user_stories") - total_points = serializers.Field(source="total_points_attr") - closed_points = serializers.Field(source="closed_points_attr") + total_points = serpy.MethodField() + closed_points = serpy.MethodField() def get_user_stories(self, obj): return UserStoryListSerializer(obj.user_stories.all(), many=True).data + + def get_total_points(self, obj): + assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute" + return obj.total_points_attr + + def get_closed_points(self, obj): + assert hasattr(obj, "closed_points_attr"), "instance must have a closed_points_attr attribute" + return obj.closed_points_attr diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index 2d788298..a47d9bed 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -44,7 +44,7 @@ class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer): return attrs -class CachedSerializedUsersMixin(serpy.Serializer): +class ListCachedUsersSerializerMixin(serpy.Serializer): def to_value(self, instance): self._serialized_users = {} return super().to_value(instance) @@ -61,7 +61,7 @@ class CachedSerializedUsersMixin(serpy.Serializer): return serialized_user -class OwnerExtraInfoMixin(CachedSerializedUsersMixin): +class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): owner = serpy.Field(attr="owner_id") owner_extra_info = serpy.MethodField() @@ -69,7 +69,7 @@ class OwnerExtraInfoMixin(CachedSerializedUsersMixin): return self.get_user_extra_info(obj.owner) -class AssigedToExtraInfoMixin(CachedSerializedUsersMixin): +class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): assigned_to = serpy.Field(attr="assigned_to_id") assigned_to_extra_info = serpy.MethodField() @@ -77,9 +77,10 @@ class AssigedToExtraInfoMixin(CachedSerializedUsersMixin): return self.get_user_extra_info(obj.assigned_to) -class StatusExtraInfoMixin(serpy.Serializer): +class ListStatusExtraInfoSerializerMixin(serpy.Serializer): status = serpy.Field(attr="status_id") status_extra_info = serpy.MethodField() + def to_value(self, instance): self._serialized_status = {} return super().to_value(instance) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index b7c13ab1..d7723a26 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -25,6 +25,8 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin + +from taiga.projects.attachments.utils import attach_basic_attachments from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, TaskStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin @@ -94,13 +96,19 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa def get_queryset(self): qs = super().get_queryset() qs = self.attach_votes_attrs_to_queryset(qs) - qs = qs.select_related("milestone", - "owner", - "assigned_to", - "status", - "project") + qs = qs.select_related( + "milestone", + "owner", + "assigned_to", + "status", + "project") - return self.attach_watchers_attrs_to_queryset(qs) + qs = self.attach_watchers_attrs_to_queryset(qs) + if "include_attachments" in self.request.QUERY_PARAMS: + qs = attach_basic_attachments(qs) + qs = qs.extra(select={"include_attachments": "True"}) + + return qs def pre_save(self, obj): if obj.user_story: diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index d7423e66..ac82c570 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -23,10 +23,12 @@ from taiga.base.api import serializers from taiga.base.fields import PgArrayField from taiga.base.neighbors import NeighborsSerializerMixin +from taiga.mdrender.service import render as mdrender +from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.mixins.serializers import OwnerExtraInfoMixin -from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin -from taiga.projects.mixins.serializers import StatusExtraInfoMixin +from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator @@ -46,8 +48,10 @@ from . import models import serpy + class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, serializers.ModelSerializer): + tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) comment = serializers.SerializerMethodField("get_comment") @@ -83,8 +87,10 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, + ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, + ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin, serializers.LightSerializer): + id = serpy.Field() user_story = serpy.Field(attr="user_story_id") ref = serpy.Field() diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index dbe5c433..0c54f81d 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -36,6 +36,7 @@ from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet from taiga.base.api.utils import get_object_or_404 +from taiga.projects.attachments.utils import attach_basic_attachments from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.services import take_snapshot from taiga.projects.milestones.models import Milestone @@ -49,6 +50,7 @@ from taiga.projects.votes.mixins.viewsets import VotedResourceMixin from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin from taiga.projects.userstories.utils import attach_total_points from taiga.projects.userstories.utils import attach_role_points +from taiga.projects.userstories.utils import attach_tasks from . import models from . import permissions @@ -105,10 +107,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "owner", "assigned_to", "generated_from_issue") + qs = self.attach_votes_attrs_to_queryset(qs) qs = self.attach_watchers_attrs_to_queryset(qs) qs = attach_total_points(qs) qs = attach_role_points(qs) + + if "include_attachments" in self.request.QUERY_PARAMS: + qs = attach_basic_attachments(qs) + qs = qs.extra(select={"include_attachments": "True"}) + + if "include_tasks" in self.request.QUERY_PARAMS: + qs = attach_tasks(qs) + qs = qs.extra(select={"include_tasks": "True"}) + return qs def pre_conditions_on_save(self, obj): diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index c863c87b..236a54d8 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -29,20 +29,19 @@ from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.utils import json from taiga.mdrender.service import render as mdrender - +from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin from taiga.projects.milestones.validators import SprintExistsValidator +from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin from taiga.projects.models import Project, UserStoryStatus -from taiga.projects.mixins.serializers import OwnerExtraInfoMixin -from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin -from taiga.projects.mixins.serializers import StatusExtraInfoMixin from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.serializers import BasicUserStoryStatusSerializer from taiga.projects.tagging.fields import TagsAndTagsColorsField from taiga.projects.userstories.validators import UserStoryExistsValidator -from taiga.projects.validators import ProjectExistsValidator -from taiga.projects.validators import UserStoryStatusExistsValidator +from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin @@ -136,7 +135,9 @@ class ListOriginIssueSerializer(serializers.LightSerializer): class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, serializers.LightSerializer): + ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, + ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin, + serializers.LightSerializer): id = serpy.Field() ref = serpy.Field() @@ -163,13 +164,11 @@ class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResour is_blocked = serpy.Field() blocked_note = serpy.Field() tags = serpy.Field() - total_points = serpy.Field("total_points_attr") + total_points = serpy.MethodField() comment = serpy.MethodField("get_comment") origin_issue = ListOriginIssueSerializer(attr="generated_from_issue") - def to_value(self, instance): - self._serialized_status = {} - return super().to_value(instance) + tasks = serpy.MethodField() def get_milestone_slug(self, obj): return obj.milestone.slug if obj.milestone else None @@ -177,15 +176,31 @@ class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResour def get_milestone_name(self, obj): return obj.milestone.name if obj.milestone else None + def get_total_points(self, obj): + assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute" + return obj.total_points_attr + def get_points(self, obj): + assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute" if obj.role_points_attr is None: return {} - return dict(ChainMap(*json.loads(obj.role_points_attr))) + return dict(ChainMap(*obj.role_points_attr)) def get_comment(self, obj): return "" + def get_tasks(self, obj): + include_tasks = getattr(obj, "include_tasks", False) + + if include_tasks: + assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute" + + if not include_tasks or obj.tasks_attr is None: + return [] + + return obj.tasks_attr + class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): def serialize_neighbor(self, neighbor): diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index 36a9970d..809248f7 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -46,11 +46,39 @@ def attach_role_points(queryset, as_field="role_points_attr"): :return: Queryset object with the additional `as_field` field. """ model = queryset.model - sql = """SELECT json_agg(json_build_object(userstories_rolepoints.role_id, - userstories_rolepoints.points_id))::text + sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id)) FROM userstories_rolepoints WHERE userstories_rolepoints.user_story_id = {tbl}.id""" sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset + + +def attach_tasks(queryset, as_field="tasks_attr"): + """Attach tasks as json column to each object of the queryset. + + :param queryset: A Django user stories queryset object. + :param as_field: Attach the role points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + + model = queryset.model + sql = """SELECT json_agg(row_to_json(t)) + FROM( + SELECT + tasks_task.id, + tasks_task.ref, + tasks_task.subject, + tasks_task.status_id, + tasks_task.is_blocked, + tasks_task.is_iocaine, + projects_taskstatus.is_closed + FROM tasks_task + INNER JOIN projects_taskstatus on projects_taskstatus.id = tasks_task.status_id + WHERE user_story_id = {tbl}.id) t""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index fcecee3b..072c0595 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -185,3 +185,24 @@ def test_custom_fields_csv_generation(): assert row[24] == attr.name row = next(reader) assert row[24] == "val1" + + +def test_get_tasks_including_attachments(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + task = f.TaskFactory.create(project=project) + f.TaskAttachmentFactory(project=project, content_object=task) + url = reverse("tasks-list") + + client.login(project.owner) + + response = client.get(url) + assert response.status_code == 200 + assert response.data[0].get("attachments") == [] + + url = reverse("tasks-list") + "?include_attachments=1" + response = client.get(url) + assert response.status_code == 200 + assert len(response.data[0].get("attachments")) == 1 diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index bc3c5560..e05fff68 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -644,3 +644,45 @@ def test_update_userstory_update_tribe_gig(client): assert response.status_code == 200 assert response.data["tribe_gig"] == data["tribe_gig"] + + +def test_get_user_stories_including_tasks(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + user_story = f.UserStoryFactory.create(project=project) + f.TaskFactory.create(user_story=user_story) + url = reverse("userstories-list") + + client.login(project.owner) + + response = client.get(url) + assert response.status_code == 200 + assert response.data[0].get("tasks") == [] + + url = reverse("userstories-list") + "?include_tasks=1" + response = client.get(url) + assert response.status_code == 200 + assert len(response.data[0].get("tasks")) == 1 + + +def test_get_user_stories_including_attachments(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + user_story = f.UserStoryFactory.create(project=project) + f.UserStoryAttachmentFactory(project=project, content_object=user_story) + url = reverse("userstories-list") + + client.login(project.owner) + + response = client.get(url) + assert response.status_code == 200 + assert response.data[0].get("attachments") == [] + + url = reverse("userstories-list") + "?include_attachments=1" + response = client.get(url) + assert response.status_code == 200 + assert len(response.data[0].get("attachments")) == 1 From 13e0e79f4432c1432c233cc8cc0d9d63b97ced0a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 21 Jun 2016 14:04:15 +0200 Subject: [PATCH 071/261] Fixing filter_data endpoint for userstories and issues --- taiga/projects/issues/services.py | 49 +++++++++++++++++++++----- taiga/projects/userstories/services.py | 49 +++++++++++++++++++++----- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index a494b1f4..3ebcec7a 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -423,16 +423,47 @@ def _get_issues_owners(project, queryset): return sorted(result, key=itemgetter("full_name")) -def _get_issues_tags(queryset): - tags = [] - for t_list in queryset.values_list("tags", flat=True): - if t_list is None: - continue - tags += list(t_list) +def _get_issues_tags(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] - tags = [{"name":e, "count":tags.count(e)} for e in set(tags)] + extra_sql = """ + WITH + issues_tags AS ( + SELECT tag, COUNT(tag) counter FROM ( + SELECT UNNEST(tags) tag + FROM issues_issue + WHERE {where} + ) tags + GROUP BY tag + ), + project_tags AS ( + SELECT reduce_dim(tags_colors) tag_color + FROM projects_project + WHERE id=%s + ) - return sorted(tags, key=itemgetter("name")) + SELECT + tag_color[1] tag, issues_tags.counter counter + FROM project_tags + LEFT JOIN + issues_tags ON project_tags.tag_color[1] = issues_tags.tag + ORDER BY tag + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for name, count in rows: + result.append({ + "name": name, + "count": 0 if count is None else count, + }) + return result def get_issues_filters_data(project, querysets): @@ -447,7 +478,7 @@ def get_issues_filters_data(project, querysets): ("severities", _get_issues_severities(project, querysets["severities"])), ("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])), ("owners", _get_issues_owners(project, querysets["owners"])), - ("tags", _get_issues_tags(querysets["tags"])), + ("tags", _get_issues_tags(project, querysets["tags"])), ]) return data diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 5ce47635..1e2e11bf 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -379,16 +379,47 @@ def _get_userstories_owners(project, queryset): return sorted(result, key=itemgetter("full_name")) -def _get_userstories_tags(queryset): - tags = [] - for t_list in queryset.values_list("tags", flat=True): - if t_list is None: - continue - tags += list(t_list) +def _get_userstories_tags(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] - tags = [{"name":e, "count":tags.count(e)} for e in set(tags)] + extra_sql = """ + WITH + userstories_tags AS ( + SELECT tag, COUNT(tag) counter FROM ( + SELECT UNNEST(tags) tag + FROM userstories_userstory + WHERE {where} + ) tags + GROUP BY tag + ), + project_tags AS ( + SELECT reduce_dim(tags_colors) tag_color + FROM projects_project + WHERE id=%s + ) - return sorted(tags, key=itemgetter("name")) + SELECT + tag_color[1] tag, userstories_tags.counter counter + FROM project_tags + LEFT JOIN + userstories_tags ON project_tags.tag_color[1] = userstories_tags.tag + ORDER BY tag + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for name, count in rows: + result.append({ + "name": name, + "count": 0 if count is None else count, + }) + return result def get_userstories_filters_data(project, querysets): @@ -400,7 +431,7 @@ def get_userstories_filters_data(project, querysets): ("statuses", _get_userstories_statuses(project, querysets["statuses"])), ("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])), ("owners", _get_userstories_owners(project, querysets["owners"])), - ("tags", _get_userstories_tags(querysets["tags"])), + ("tags", _get_userstories_tags(project, querysets["tags"])), ]) return data From c82288faa3517b9ad9c67bc55bd4774551399328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 22 Jun 2016 22:24:07 +0200 Subject: [PATCH 072/261] Fix tests, identations and pass the Flake8 --- taiga/projects/issues/services.py | 47 ++++++++++-------- taiga/projects/tasks/permissions.py | 1 + taiga/projects/tasks/services.py | 2 +- taiga/projects/userstories/api.py | 3 -- taiga/projects/userstories/services.py | 68 +++++++++++++++----------- tests/integration/test_issues.py | 7 ++- tests/integration/test_userstories.py | 6 +-- 7 files changed, 73 insertions(+), 61 deletions(-) diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index 3ebcec7a..7786d0da 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -35,6 +35,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset from . import models +##################################################### +# Bulk actions +##################################################### + def get_issues_from_bulk(bulk_data, **additional_fields): """Convert `bulk_data` into a list of issues. @@ -83,6 +87,10 @@ def update_issues_order_in_bulk(bulk_data): db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue) +##################################################### +# CSV +##################################################### + def issues_to_csv(project, queryset): csv_data = io.StringIO() fieldnames = ["ref", "subject", "description", "sprint", "sprint_estimated_start", @@ -143,6 +151,10 @@ def issues_to_csv(project, queryset): return csv_data +##################################################### +# Api filter data +##################################################### + def _get_issues_statuses(project, queryset): compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) @@ -394,7 +406,7 @@ def _get_issues_owners(project, queryset): FROM projects_membership LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") - WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL) + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL -- System users UNION @@ -430,27 +442,22 @@ def _get_issues_tags(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH - issues_tags AS ( - SELECT tag, COUNT(tag) counter FROM ( - SELECT UNNEST(tags) tag - FROM issues_issue - WHERE {where} - ) tags - GROUP BY tag - ), - project_tags AS ( - SELECT reduce_dim(tags_colors) tag_color - FROM projects_project - WHERE id=%s - ) + WITH issues_tags AS ( + SELECT tag, + COUNT(tag) counter FROM ( + SELECT UNNEST(tags) tag + FROM issues_issue + WHERE {where}) tags + GROUP BY tag), + project_tags AS ( + SELECT reduce_dim(tags_colors) tag_color + FROM projects_project + WHERE id=%s) - SELECT - tag_color[1] tag, issues_tags.counter counter + SELECT tag_color[1] tag, issues_tags.counter counter FROM project_tags - LEFT JOIN - issues_tags ON project_tags.tag_color[1] = issues_tags.tag - ORDER BY tag + LEFT JOIN issues_tags ON project_tags.tag_color[1] = issues_tags.tag + ORDER BY tag """.format(where=where) with closing(connection.cursor()) as cursor: diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 8cf40dd7..a1cbdfe1 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -31,6 +31,7 @@ class TaskPermission(TaigaResourcePermission): partial_update_perms = CommentAndOrUpdatePerm('modify_task', 'comment_task') destroy_perms = HasProjectPerm('delete_task') list_perms = AllowAny() + filters_data_perms = AllowAny() csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_task') bulk_update_order_perms = HasProjectPerm('modify_task') diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 427e4f28..5729f588 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -144,7 +144,7 @@ def tasks_to_csv(project, queryset): "voters": task.total_voters, "created_date": task.created_date, "modified_date": task.modified_date, - "finished_date": task.finished_date, + "finished_date": task.finished_date, } for custom_attr in custom_attrs: value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 0c54f81d..87ecf18b 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -87,9 +87,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "kanban_order", "total_voters"] - # Specific filter used for filtering neighbor user stories - _neighbor_tags_filter = filters.TagsFilter('neighbor_tags') - def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: return serializers.UserStoryNeighborsSerializer diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 1e2e11bf..d867f5e6 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -28,9 +28,8 @@ from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot -from taiga.projects.userstories.apps import ( - connect_userstories_signals, - disconnect_userstories_signals) +from taiga.projects.userstories.apps import connect_userstories_signals +from taiga.projects.userstories.apps import disconnect_userstories_signals from taiga.events import events from taiga.projects.votes.utils import attach_total_voters_to_queryset @@ -39,6 +38,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset from . import models +##################################################### +# Bulk actions +##################################################### + def get_userstories_from_bulk(bulk_data, **additional_fields): """Convert `bulk_data` into a list of user stories. @@ -72,7 +75,7 @@ def create_userstories_in_bulk(bulk_data, callback=None, precall=None, **additio return userstories -def update_userstories_order_in_bulk(bulk_data:list, field:str, project:object): +def update_userstories_order_in_bulk(bulk_data: list, field: str, project: object): """ Update the order of some user stories. `bulk_data` should be a list of tuples with the following format: @@ -92,7 +95,7 @@ def update_userstories_order_in_bulk(bulk_data:list, field:str, project:object): db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory) -def update_userstories_milestone_in_bulk(bulk_data:list, milestone:object): +def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object): """ Update the milestone of some user stories. `bulk_data` should be a list of user story ids: @@ -108,7 +111,6 @@ def update_userstories_milestone_in_bulk(bulk_data:list, milestone:object): def snapshot_userstories_in_bulk(bulk_data, user): - user_story_ids = [] for us_data in bulk_data: try: us = models.UserStory.objects.get(pk=us_data['us_id']) @@ -117,6 +119,10 @@ def snapshot_userstories_in_bulk(bulk_data, user): pass +##################################################### +# Open/Close calcs +##################################################### + def calculate_userstory_is_closed(user_story): if user_story.status is None: return False @@ -144,7 +150,11 @@ def open_userstory(us): us.save(update_fields=["is_closed", "finish_date"]) -def userstories_to_csv(project,queryset): +##################################################### +# CSV +##################################################### + +def userstories_to_csv(project, queryset): csv_data = io.StringIO() fieldnames = ["ref", "subject", "description", "sprint", "sprint_estimated_start", "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", @@ -160,7 +170,7 @@ def userstories_to_csv(project,queryset): "created_date", "modified_date", "finish_date", "client_requirement", "team_requirement", "attachments", "generated_from_issue", "external_reference", "tasks", - "tags","watchers", "voters"] + "tags", "watchers", "voters"] custom_attrs = project.userstorycustomattributes.all() for custom_attr in custom_attrs: @@ -230,6 +240,10 @@ def userstories_to_csv(project,queryset): return csv_data +##################################################### +# Api filter data +##################################################### + def _get_userstories_statuses(project, queryset): compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) @@ -336,7 +350,8 @@ def _get_userstories_owners(project, queryset): extra_sql = """ WITH counters AS ( - SELECT "userstories_userstory"."owner_id" owner_id, count(coalesce("userstories_userstory"."owner_id", -1)) count + SELECT "userstories_userstory"."owner_id" owner_id, + count(coalesce("userstories_userstory"."owner_id", -1)) count FROM "userstories_userstory" INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") WHERE {where} @@ -350,7 +365,7 @@ def _get_userstories_owners(project, queryset): FROM projects_membership LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") - WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL) + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL -- System users UNION @@ -386,27 +401,22 @@ def _get_userstories_tags(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH - userstories_tags AS ( - SELECT tag, COUNT(tag) counter FROM ( - SELECT UNNEST(tags) tag - FROM userstories_userstory - WHERE {where} - ) tags - GROUP BY tag - ), - project_tags AS ( - SELECT reduce_dim(tags_colors) tag_color - FROM projects_project - WHERE id=%s - ) + WITH userstories_tags AS ( + SELECT tag, + COUNT(tag) counter FROM ( + SELECT UNNEST(tags) tag + FROM userstories_userstory + WHERE {where}) tags + GROUP BY tag), + project_tags AS ( + SELECT reduce_dim(tags_colors) tag_color + FROM projects_project + WHERE id=%s) - SELECT - tag_color[1] tag, userstories_tags.counter counter + SELECT tag_color[1] tag, userstories_tags.counter counter FROM project_tags - LEFT JOIN - userstories_tags ON project_tags.tag_color[1] = userstories_tags.tag - ORDER BY tag + LEFT JOIN userstories_tags ON project_tags.tag_color[1] = userstories_tags.tag + ORDER BY tag """.format(where=where) with closing(connection.cursor()) as cursor: diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index a14b2db4..4ea78a35 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -229,6 +229,7 @@ def test_api_filter_by_text_6(client): assert response.status_code == 200 assert number_of_issues == 1 + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) @@ -378,8 +379,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 @@ -415,8 +415,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index e05fff68..7eac9b06 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -504,8 +504,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 @@ -528,8 +527,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 - with pytest.raises(StopIteration): - assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 From 4904e1859fbf086a74e417f085163ca59cf70259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 23 Jun 2016 18:23:40 +0200 Subject: [PATCH 073/261] Fix api calls from the front --- taiga/projects/issues/services.py | 10 ++++++---- taiga/projects/userstories/services.py | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index 7786d0da..56790e82 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -445,8 +445,10 @@ def _get_issues_tags(project, queryset): WITH issues_tags AS ( SELECT tag, COUNT(tag) counter FROM ( - SELECT UNNEST(tags) tag + SELECT UNNEST(issues_issue.tags) tag FROM issues_issue + INNER JOIN projects_project + ON (issues_issue.project_id = projects_project.id) WHERE {where}) tags GROUP BY tag), project_tags AS ( @@ -454,7 +456,7 @@ def _get_issues_tags(project, queryset): FROM projects_project WHERE id=%s) - SELECT tag_color[1] tag, issues_tags.counter counter + SELECT tag_color[1] tag, COALESCE(issues_tags.counter, 0) counter FROM project_tags LEFT JOIN issues_tags ON project_tags.tag_color[1] = issues_tags.tag ORDER BY tag @@ -468,9 +470,9 @@ def _get_issues_tags(project, queryset): for name, count in rows: result.append({ "name": name, - "count": 0 if count is None else count, + "count": count, }) - return result + return sorted(result, key=itemgetter("name")) def get_issues_filters_data(project, querysets): diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index d867f5e6..61fe52ec 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -404,8 +404,10 @@ def _get_userstories_tags(project, queryset): WITH userstories_tags AS ( SELECT tag, COUNT(tag) counter FROM ( - SELECT UNNEST(tags) tag + SELECT UNNEST(userstories_userstory.tags) tag FROM userstories_userstory + INNER JOIN projects_project + ON (userstories_userstory.project_id = projects_project.id) WHERE {where}) tags GROUP BY tag), project_tags AS ( @@ -413,7 +415,7 @@ def _get_userstories_tags(project, queryset): FROM projects_project WHERE id=%s) - SELECT tag_color[1] tag, userstories_tags.counter counter + SELECT tag_color[1] tag, COALESCE(userstories_tags.counter, 0) counter FROM project_tags LEFT JOIN userstories_tags ON project_tags.tag_color[1] = userstories_tags.tag ORDER BY tag @@ -427,9 +429,9 @@ def _get_userstories_tags(project, queryset): for name, count in rows: result.append({ "name": name, - "count": 0 if count is None else count, + "count": count, }) - return result + return sorted(result, key=itemgetter("name")) def get_userstories_filters_data(project, querysets): From 6b6f6e80bec17e48e81cd56b7fcfbd6c0c7bba08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 22 Jun 2016 22:24:52 +0200 Subject: [PATCH 074/261] Add filter_data to tasks API endpoint --- taiga/projects/tasks/api.py | 104 +++++++++----- taiga/projects/tasks/services.py | 233 ++++++++++++++++++++++++++++++- tests/integration/test_tasks.py | 129 +++++++++++++++++ 3 files changed, 422 insertions(+), 44 deletions(-) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index d7723a26..01ae057e 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -44,8 +44,18 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) - filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter) - retrieve_exclude_filters = (filters.WatchersFilter,) + filter_backends = (filters.CanViewTasksFilterBackend, + filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.WatchersFilter, + filters.QFilter) + retrieve_exclude_filters = (filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.WatchersFilter) filter_fields = ["user_story", "milestone", "project", @@ -62,6 +72,44 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa return serializers.TaskSerializer + def get_queryset(self): + qs = super().get_queryset() + qs = self.attach_votes_attrs_to_queryset(qs) + qs = qs.select_related("milestone", + "project", + "status", + "owner", + "assigned_to") + + qs = self.attach_watchers_attrs_to_queryset(qs) + if "include_attachments" in self.request.QUERY_PARAMS: + qs = attach_basic_attachments(qs) + qs = qs.extra(select={"include_attachments": "True"}) + + return qs + + def pre_conditions_on_save(self, obj): + super().pre_conditions_on_save(obj) + + if obj.milestone and obj.milestone.project != obj.project: + raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) + + if obj.user_story and obj.user_story.project != obj.project: + raise exc.WrongArguments(_("You don't have permissions to set this user story to this task.")) + + if obj.status and obj.status.project != obj.project: + raise exc.WrongArguments(_("You don't have permissions to set this status to this task.")) + + if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone: + raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) + + def pre_save(self, obj): + if obj.user_story: + obj.milestone = obj.user_story.milestone + if not obj.id: + obj.owner = self.request.user + super().pre_save(obj) + def update(self, request, *args, **kwargs): self.object = self.get_object_or_none() project_id = request.DATA.get('project', None) @@ -93,44 +141,24 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa return super().update(request, *args, **kwargs) - def get_queryset(self): - qs = super().get_queryset() - qs = self.attach_votes_attrs_to_queryset(qs) - qs = qs.select_related( - "milestone", - "owner", - "assigned_to", - "status", - "project") + @list_route(methods=["GET"]) + def filters_data(self, request, *args, **kwargs): + project_id = request.QUERY_PARAMS.get("project", None) + project = get_object_or_404(Project, id=project_id) - qs = self.attach_watchers_attrs_to_queryset(qs) - if "include_attachments" in self.request.QUERY_PARAMS: - qs = attach_basic_attachments(qs) - qs = qs.extra(select={"include_attachments": "True"}) + filter_backends = self.get_filter_backends() + statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) + assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) + owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) - return qs - - def pre_save(self, obj): - if obj.user_story: - obj.milestone = obj.user_story.milestone - if not obj.id: - obj.owner = self.request.user - super().pre_save(obj) - - def pre_conditions_on_save(self, obj): - super().pre_conditions_on_save(obj) - - if obj.milestone and obj.milestone.project != obj.project: - raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) - - if obj.user_story and obj.user_story.project != obj.project: - raise exc.WrongArguments(_("You don't have permissions to set this user story to this task.")) - - if obj.status and obj.status.project != obj.project: - raise exc.WrongArguments(_("You don't have permissions to set this status to this task.")) - - if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone: - raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) + queryset = self.get_queryset() + querysets = { + "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), + "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), + "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), + "tags": self.filter_queryset(queryset) + } + return response.Ok(services.get_tasks_filters_data(project, querysets)) @list_route(methods=["GET"]) def by_ref(self, request): diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 5729f588..ac7a6478 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -16,14 +16,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import io import csv +import io +from collections import OrderedDict +from operator import itemgetter +from contextlib import closing + +from django.db import connection +from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot -from taiga.projects.tasks.apps import ( - connect_tasks_signals, - disconnect_tasks_signals) +from taiga.projects.tasks.apps import connect_tasks_signals +from taiga.projects.tasks.apps import disconnect_tasks_signals from taiga.events import events from taiga.projects.votes.utils import attach_total_voters_to_queryset from taiga.projects.notifications.utils import attach_watchers_to_queryset @@ -31,6 +36,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset from . import models +##################################################### +# Bulk actions +##################################################### + def get_tasks_from_bulk(bulk_data, **additional_fields): """Convert `bulk_data` into a list of tasks. @@ -64,7 +73,7 @@ def create_tasks_in_bulk(bulk_data, callback=None, precall=None, **additional_fi return tasks -def update_tasks_order_in_bulk(bulk_data:list, field:str, project:object): +def update_tasks_order_in_bulk(bulk_data: list, field: str, project: object): """ Update the order of some tasks. `bulk_data` should be a list of tuples with the following format: @@ -85,7 +94,6 @@ def update_tasks_order_in_bulk(bulk_data:list, field:str, project:object): def snapshot_tasks_in_bulk(bulk_data, user): - task_ids = [] for task_data in bulk_data: try: task = models.Task.objects.get(pk=task_data['task_id']) @@ -94,6 +102,10 @@ def snapshot_tasks_in_bulk(bulk_data, user): pass +##################################################### +# CSV +##################################################### + def tasks_to_csv(project, queryset): csv_data = io.StringIO() fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start", @@ -153,3 +165,212 @@ def tasks_to_csv(project, queryset): writer.writerow(task_data) return csv_data + + +##################################################### +# Api filter data +##################################################### + +def _get_tasks_statuses(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_taskstatus"."id", + "projects_taskstatus"."name", + "projects_taskstatus"."color", + "projects_taskstatus"."order", + (SELECT count(*) + FROM "tasks_task" + INNER JOIN "projects_project" ON + ("tasks_task"."project_id" = "projects_project"."id") + WHERE {where} AND "tasks_task"."status_id" = "projects_taskstatus"."id") + FROM "projects_taskstatus" + WHERE "projects_taskstatus"."project_id" = %s + ORDER BY "projects_taskstatus"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_tasks_assigned_to(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH counters AS ( + SELECT assigned_to_id, count(assigned_to_id) count + FROM "tasks_task" + INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id") + WHERE {where} AND "tasks_task"."assigned_to_id" IS NOT NULL + GROUP BY assigned_to_id + ) + + SELECT "projects_membership"."user_id" user_id, + "users_user"."full_name", + "users_user"."username", + COALESCE("counters".count, 0) count + FROM projects_membership + LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id") + INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL + + -- unassigned tasks + UNION + + SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count + FROM "tasks_task" + INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id") + WHERE {where} AND "tasks_task"."assigned_to_id" IS NULL + GROUP BY assigned_to_id + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id] + where_params) + rows = cursor.fetchall() + + result = [] + none_valued_added = False + for id, full_name, username, count in rows: + result.append({ + "id": id, + "full_name": full_name or username or "", + "count": count, + }) + + if id is None: + none_valued_added = True + + # If there was no task with null assigned_to we manually add it + if not none_valued_added: + result.append({ + "id": None, + "full_name": "", + "count": 0, + }) + + return sorted(result, key=itemgetter("full_name")) + + +def _get_tasks_owners(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH counters AS ( + SELECT "tasks_task"."owner_id" owner_id, + count(coalesce("tasks_task"."owner_id", -1)) count + FROM "tasks_task" + INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id") + WHERE {where} + GROUP BY "tasks_task"."owner_id" + ) + + SELECT "projects_membership"."user_id" id, + "users_user"."full_name", + "users_user"."username", + COALESCE("counters".count, 0) count + FROM projects_membership + LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") + INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL + + -- System users + UNION + + SELECT "users_user"."id" user_id, + "users_user"."full_name" full_name, + "users_user"."username" username, + COALESCE("counters".count, 0) count + FROM users_user + LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id") + WHERE ("users_user"."is_system" IS TRUE) + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, username, count in rows: + if count > 0: + result.append({ + "id": id, + "full_name": full_name or username or "", + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_tasks_tags(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH tasks_tags AS ( + SELECT tag, + COUNT(tag) counter FROM ( + SELECT UNNEST(tasks_task.tags) tag + FROM tasks_task + INNER JOIN projects_project + ON (tasks_task.project_id = projects_project.id) + WHERE {where}) tags + GROUP BY tag), + project_tags AS ( + SELECT reduce_dim(tags_colors) tag_color + FROM projects_project + WHERE id=%s) + + SELECT tag_color[1] tag, COALESCE(tasks_tags.counter, 0) counter + FROM project_tags + LEFT JOIN tasks_tags ON project_tags.tag_color[1] = tasks_tags.tag + ORDER BY tag + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for name, count in rows: + result.append({ + "name": name, + "count": count, + }) + return sorted(result, key=itemgetter("name")) + + +def get_tasks_filters_data(project, querysets): + """ + Given a project and an tasks queryset, return a simple data structure + of all possible filters for the tasks in the queryset. + """ + data = OrderedDict([ + ("statuses", _get_tasks_statuses(project, querysets["statuses"])), + ("assigned_to", _get_tasks_assigned_to(project, querysets["assigned_to"])), + ("owners", _get_tasks_owners(project, querysets["owners"])), + ("tags", _get_tasks_tags(project, querysets["tags"])), + ]) + + return data diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 072c0595..c12e1ecb 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -206,3 +206,132 @@ def test_get_tasks_including_attachments(client): response = client.get(url) assert response.status_code == 200 assert len(response.data[0].get("attachments")) == 1 + + +def test_api_filters_data(client): + project = f.ProjectFactory.create() + user1 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user1, project=project) + user2 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user2, project=project) + user3 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user3, project=project) + + status0 = f.TaskStatusFactory.create(project=project) + status1 = f.TaskStatusFactory.create(project=project) + status2 = f.TaskStatusFactory.create(project=project) + status3 = f.TaskStatusFactory.create(project=project) + + tag0 = "test1test2test3" + tag1 = "test1" + tag2 = "test2" + tag3 = "test3" + + # ------------------------------------------------------ + # | Task | Owner | Assigned To | Tags | + # |-------#--------#-------------#---------------------| + # | 0 | user2 | None | tag1 | + # | 1 | user1 | None | tag2 | + # | 2 | user3 | None | tag1 tag2 | + # | 3 | user2 | None | tag3 | + # | 4 | user1 | user1 | tag1 tag2 tag3 | + # | 5 | user3 | user1 | tag3 | + # | 6 | user2 | user1 | tag1 tag2 | + # | 7 | user1 | user2 | tag3 | + # | 8 | user3 | user2 | tag1 | + # | 9 | user2 | user3 | tag0 | + # ------------------------------------------------------ + + task0 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + task1 = f.TaskFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + task2 = f.TaskFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + task3 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + task4 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + task5 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + task6 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + task7 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + task8 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + task9 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) + + url = reverse("tasks-filters-data") + "?project={}".format(project.id) + + client.login(user1) + + ## No filter + response = client.get(url) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 5 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + + ## Filter ((status0 or status3) + response = client.get(url + "&status={},{}".format(status3.id, status0.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 + + ## Filter ((tag1 and tag2) and (user1 or user2)) + response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 From 9709e6a2593f1579bb5a3f44fab2f550eaf123f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 21 Jun 2016 13:44:00 +0200 Subject: [PATCH 075/261] Enhancement#4356: Add gzipped dumps import support --- CHANGELOG.md | 4 +++- taiga/export_import/api.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bdf62a..76ed1d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,9 @@ - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. - Include created, modified and finished dates for tasks in CSV reports - User stories and tasks listing API call support extra params to include more data (tasks and attachemnts and attachments, respectively) - +- Import/Export: + - Gzip export/import support. + - Export performance improvements. ### Misc - [API] Improve performance of some calls over list. diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index a8a1dddd..d8453ad5 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -318,6 +318,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if not dump: raise exc.WrongArguments(_("Needed dump file")) + if dump.content_type == "application/gzip": + dump = gzip.GzipFile(fileobj=dump) + reader = codecs.getreader("utf-8") try: From b80e4566f6bf37a78a715dd04ede018c04b9a093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 22 Jun 2016 16:52:56 +0200 Subject: [PATCH 076/261] Compress diffs to show only the changes and some context --- CHANGELOG.md | 1 + taiga/mdrender/service.py | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ed1d66..293cb3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add gravatar url to Users API endpoint. - ProjectTemplates now are sorted by the attribute 'order'. - Create enpty wiki pages (if not exist) when a new link is created. +- Diff messages in history entries now show only the relevant changes (with some context). - Comments: - Now comment owners and project admins can edit existing comments with the history Entry endpoint. - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. diff --git a/taiga/mdrender/service.py b/taiga/mdrender/service.py index cc87e25b..701ed0d4 100644 --- a/taiga/mdrender/service.py +++ b/taiga/mdrender/service.py @@ -126,16 +126,42 @@ def render_and_extract(project, text): class DiffMatchPatch(diff_match_patch.diff_match_patch): def diff_pretty_html(self, diffs): + def _sanitize_text(text): + return (text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "
")) + + def _split_long_text(text, idx, size): + splited_text = text.split() + + if len(splited_text) > 25: + if idx == 0: + # The first is (...)text + first = "" + else: + first = " ".join(splited_text[:10]) + + if idx != 0 and idx == size - 1: + # The last is text(...) + last = "" + else: + last = " ".join(splited_text[-10:]) + + return "{}(...){}".format(first, last) + return text + + size = len(diffs) html = [] - for (op, data) in diffs: - text = (data.replace("&", "&").replace("<", "<") - .replace(">", ">").replace("\n", "
")) + for idx, (op, data) in enumerate(diffs): if op == self.DIFF_INSERT: - html.append("%s" % text) + text = _sanitize_text(data) + html.append("{}".format(text)) elif op == self.DIFF_DELETE: - html.append("%s" % text) + text = _sanitize_text(data) + html.append("{}".format(text)) elif op == self.DIFF_EQUAL: - html.append("%s" % text) + text = _split_long_text(_sanitize_text(data), idx, size) + html.append("{}".format(text)) + return "".join(html) From 713e7dffda751cb5e0ef1e64533ddf948a81b960 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 30 Jun 2016 11:54:15 +0200 Subject: [PATCH 077/261] Fixing MembersFilterBackend --- taiga/base/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index e26e7911..d9369cbd 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -268,7 +268,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend): qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission]) - return qs + return qs.distinct() ##################################################################### From d1411c2409ecdb21e3402cca30e20f548a552273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 30 Jun 2016 17:39:06 +0200 Subject: [PATCH 078/261] Bamedizing --- taiga/projects/issues/apps.py | 1 - taiga/projects/tasks/apps.py | 2 +- taiga/projects/userstories/apps.py | 5 ++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/taiga/projects/issues/apps.py b/taiga/projects/issues/apps.py index ac01491a..de4de986 100644 --- a/taiga/projects/issues/apps.py +++ b/taiga/projects/issues/apps.py @@ -22,7 +22,6 @@ from django.db.models import signals def connect_issues_signals(): - from taiga.projects import signals as generic_handlers from taiga.projects.tagging import signals as tagging_handlers from . import signals as handlers diff --git a/taiga/projects/tasks/apps.py b/taiga/projects/tasks/apps.py index 7ae193cc..1ad2e96d 100644 --- a/taiga/projects/tasks/apps.py +++ b/taiga/projects/tasks/apps.py @@ -22,7 +22,6 @@ from django.db.models import signals def connect_tasks_signals(): - from taiga.projects import signals as generic_handlers from taiga.projects.tagging import signals as tagging_handlers from . import signals as handlers @@ -50,6 +49,7 @@ def connect_tasks_close_or_open_us_and_milestone_signals(): sender=apps.get_model("tasks", "Task"), dispatch_uid="try_to_close_or_open_us_and_milestone_when_delete_task") + def connect_tasks_custom_attributes_signals(): from taiga.projects.custom_attributes import signals as custom_attributes_handlers signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task, diff --git a/taiga/projects/userstories/apps.py b/taiga/projects/userstories/apps.py index fca31409..d2fa8ce1 100644 --- a/taiga/projects/userstories/apps.py +++ b/taiga/projects/userstories/apps.py @@ -22,7 +22,6 @@ from django.db.models import signals def connect_userstories_signals(): - from taiga.projects import signals as generic_handlers from taiga.projects.tagging import signals as tagging_handlers from . import signals as handlers @@ -33,8 +32,8 @@ def connect_userstories_signals(): dispatch_uid='disable_task_signals') signals.post_delete.connect(handlers.enable_tasks_signals, - sender=apps.get_model("userstories", "UserStory"), - dispatch_uid='enable_tasks_signals') + sender=apps.get_model("userstories", "UserStory"), + dispatch_uid='enable_tasks_signals') # Cached prev object version signals.pre_save.connect(handlers.cached_prev_us, From 78e72c7488aae79c65c3caef733dee4fcd22e708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 29 Jun 2016 13:01:45 +0200 Subject: [PATCH 079/261] Fix dependencies on migrations --- taiga/projects/migrations/0030_auto_20151128_0757.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taiga/projects/migrations/0030_auto_20151128_0757.py b/taiga/projects/migrations/0030_auto_20151128_0757.py index 5f515029..2a8631df 100644 --- a/taiga/projects/migrations/0030_auto_20151128_0757.py +++ b/taiga/projects/migrations/0030_auto_20151128_0757.py @@ -111,6 +111,7 @@ class Migration(migrations.Migration): dependencies = [ ('projects', '0029_project_is_looking_for_people'), ('timeline', '0004_auto_20150603_1312'), + ('likes', '0001_initial'), ] operations = [ From 64d47aa61c400f5a2d15600c972405fd21c039ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 24 Jun 2016 14:23:26 +0200 Subject: [PATCH 080/261] Add project_id to all history entries --- .../migrations/0010_historyentry_project.py | 22 +++++++++++++ .../migrations/0011_auto_20160629_1036.py | 31 +++++++++++++++++++ .../migrations/0012_auto_20160629_1036.py | 20 ++++++++++++ taiga/projects/history/models.py | 1 + taiga/projects/history/services.py | 2 ++ .../test_history_resources.py | 12 +++++++ tests/integration/test_history.py | 4 +++ tests/integration/test_notifications.py | 12 +++++++ tests/integration/test_totals_projects.py | 4 +++ 9 files changed, 108 insertions(+) create mode 100644 taiga/projects/history/migrations/0010_historyentry_project.py create mode 100644 taiga/projects/history/migrations/0011_auto_20160629_1036.py create mode 100644 taiga/projects/history/migrations/0012_auto_20160629_1036.py diff --git a/taiga/projects/history/migrations/0010_historyentry_project.py b/taiga/projects/history/migrations/0010_historyentry_project.py new file mode 100644 index 00000000..0949a9a8 --- /dev/null +++ b/taiga/projects/history/migrations/0010_historyentry_project.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-24 12:19 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0048_auto_20160615_1508'), + ('history', '0009_auto_20160512_1110'), + ] + + operations = [ + migrations.AddField( + model_name='historyentry', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='projects.Project'), + ), + ] diff --git a/taiga/projects/history/migrations/0011_auto_20160629_1036.py b/taiga/projects/history/migrations/0011_auto_20160629_1036.py new file mode 100644 index 00000000..d3f1eb02 --- /dev/null +++ b/taiga/projects/history/migrations/0011_auto_20160629_1036.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-29 10:36 +from __future__ import unicode_literals + +from django.db import migrations +from taiga.projects.history.services import get_instance_from_key + + +def forward_func(apps, schema_editor): + HistoryEntry = apps.get_model("history", "HistoryEntry") + db_alias = schema_editor.connection.alias + for entry in HistoryEntry.objects.using(db_alias).all().iterator(): + instance = get_instance_from_key(entry.key) + if type(instance) == apps.get_model("projects", "Project"): + entry.project_id = instance.id + else: + entry.project_id = getattr(instance, 'project_id', None) + entry.save() + + HistoryEntry.objects.using(db_alias).filter(project_id__isnull=True).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0010_historyentry_project'), + ] + + operations = [ + migrations.RunPython(forward_func, atomic=False), + ] diff --git a/taiga/projects/history/migrations/0012_auto_20160629_1036.py b/taiga/projects/history/migrations/0012_auto_20160629_1036.py new file mode 100644 index 00000000..549d7076 --- /dev/null +++ b/taiga/projects/history/migrations/0012_auto_20160629_1036.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-29 10:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0011_auto_20160629_1036'), + ] + + operations = [ + migrations.AlterField( + model_name='historyentry', + name='project', + field=models.ForeignKey(on_delete=models.deletion.CASCADE, to='projects.Project'), + ), + ] diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index ee4868b1..558c5c25 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -49,6 +49,7 @@ class HistoryEntry(models.Model): """ id = models.CharField(primary_key=True, max_length=255, unique=True, editable=False, default=_generate_uuid) + project = models.ForeignKey("projects.Project") user = JsonField(null=True, blank=True, default=None) created_at = models.DateTimeField(default=timezone.now) diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index d4df56a5..71b5bcf8 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -314,6 +314,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): # Determine history type if delete: entry_type = HistoryType.delete + need_real_snapshot = True elif new_fobj and not old_fobj: entry_type = HistoryType.create elif new_fobj and old_fobj: @@ -340,6 +341,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): kwargs = { "user": {"pk": user_id, "name": user_name}, + "project_id": getattr(obj, 'project_id', getattr(obj, 'id', None)), "key": key, "type": entry_type, "snapshot": fdiff.snapshot if need_real_snapshot else None, diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index f9df4a0d..bf659f69 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -110,6 +110,7 @@ def data_us(data): m = type("Models", (object,), {}) m.public_user_story = f.UserStoryFactory(project=data.public_project, ref=1) m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, comment="testing public", key=make_key_from_model_object(m.public_user_story), diff={}, @@ -117,12 +118,14 @@ def data_us(data): m.private_user_story1 = f.UserStoryFactory(project=data.private_project1, ref=5) m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, comment="testing 1", key=make_key_from_model_object(m.private_user_story1), diff={}, user={"pk": data.project_member_with_perms.pk}) m.private_user_story2 = f.UserStoryFactory(project=data.private_project2, ref=9) m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, comment="testing 2", key=make_key_from_model_object(m.private_user_story2), diff={}, @@ -347,6 +350,7 @@ def data_task(data): m = type("Models", (object,), {}) m.public_task = f.TaskFactory(project=data.public_project, ref=2) m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, comment="testing public", key=make_key_from_model_object(m.public_task), diff={}, @@ -354,12 +358,14 @@ def data_task(data): m.private_task1 = f.TaskFactory(project=data.private_project1, ref=6) m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, comment="testing 1", key=make_key_from_model_object(m.private_task1), diff={}, user={"pk": data.project_member_with_perms.pk}) m.private_task2 = f.TaskFactory(project=data.private_project2, ref=10) m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, comment="testing 2", key=make_key_from_model_object(m.private_task2), diff={}, @@ -584,6 +590,7 @@ def data_issue(data): m = type("Models", (object,), {}) m.public_issue = f.IssueFactory(project=data.public_project, ref=3) m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, comment="testing public", key=make_key_from_model_object(m.public_issue), diff={}, @@ -591,12 +598,14 @@ def data_issue(data): m.private_issue1 = f.IssueFactory(project=data.private_project1, ref=7) m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, comment="testing 1", key=make_key_from_model_object(m.private_issue1), diff={}, user={"pk": data.project_member_with_perms.pk}) m.private_issue2 = f.IssueFactory(project=data.private_project2, ref=11) m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, comment="testing 2", key=make_key_from_model_object(m.private_issue2), diff={}, @@ -821,6 +830,7 @@ def data_wiki(data): m = type("Models", (object,), {}) m.public_wiki = f.WikiPageFactory(project=data.public_project, slug=4) m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, comment="testing public", key=make_key_from_model_object(m.public_wiki), diff={}, @@ -828,12 +838,14 @@ def data_wiki(data): m.private_wiki1 = f.WikiPageFactory(project=data.private_project1, slug=8) m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, comment="testing 1", key=make_key_from_model_object(m.private_wiki1), diff={}, user={"pk": data.project_member_with_perms.pk}) m.private_wiki2 = f.WikiPageFactory(project=data.private_project2, slug=12) m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, comment="testing 2", key=make_key_from_model_object(m.private_wiki2), diff={}, diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index f53c03b9..cc6d8d43 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -228,6 +228,7 @@ def test_delete_comment_by_project_owner(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) key = make_key_from_model_object(us) history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=project, comment="testing", key=key, diff={}, @@ -246,6 +247,7 @@ def test_edit_comment(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) key = make_key_from_model_object(us) history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=project, comment="testing", key=key, diff={}, @@ -278,6 +280,7 @@ def test_get_comment_versions(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) key = make_key_from_model_object(us) history_entry = f.HistoryEntryFactory.create( + project=project, type=HistoryType.change, comment="testing", key=key, @@ -307,6 +310,7 @@ def test_get_comment_versions_from_history_entry_without_comment(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) key = make_key_from_model_object(us) history_entry = f.HistoryEntryFactory.create( + project=project, type=HistoryType.change, key=key, diff={}, diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 71126b0f..661b68ff 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -354,6 +354,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai us = f.UserStoryFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -363,6 +364,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -372,6 +374,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -446,6 +449,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): task = f.TaskFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -455,6 +459,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -464,6 +469,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -538,6 +544,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): issue = f.IssueFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -547,6 +554,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -556,6 +564,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, @@ -630,6 +639,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) wiki = f.WikiPageFactory.create(project=project, owner=member2.user) history_change = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.change, @@ -639,6 +649,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) ) history_create = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="", type=HistoryType.create, @@ -648,6 +659,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) ) history_delete = f.HistoryEntryFactory.create( + project=project, user={"pk": member1.user.id}, comment="test:delete", type=HistoryType.delete, diff --git a/tests/integration/test_totals_projects.py b/tests/integration/test_totals_projects.py index e46c1b21..8d29d950 100644 --- a/tests/integration/test_totals_projects.py +++ b/tests/integration/test_totals_projects.py @@ -39,6 +39,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime us = f.UserStoryFactory.create(project=project, owner=project.owner) f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, @@ -57,6 +58,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, @@ -75,6 +77,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, @@ -93,6 +96,7 @@ def test_project_totals_updated_on_activity(client): totals_updated_datetime = project.totals_updated_datetime f.HistoryEntryFactory.create( + project=project, user={"pk": project.owner.id}, comment="", type=HistoryType.change, From ba66f3839bf1c2df729959903cd3662a5c0e5001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Sun, 3 Jul 2016 23:49:29 +0200 Subject: [PATCH 081/261] [i18n] Update locales --- taiga/locale/de/LC_MESSAGES/django.po | 109 ++++--- taiga/locale/pt_BR/LC_MESSAGES/django.po | 352 +++++++++++++---------- 2 files changed, 272 insertions(+), 189 deletions(-) diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po index 7b75e1f9..f5556358 100644 --- a/taiga/locale/de/LC_MESSAGES/django.po +++ b/taiga/locale/de/LC_MESSAGES/django.po @@ -13,13 +13,16 @@ # Sebastian Blum , 2015 # Silsha Fux , 2015 # Thomas McWork , 2015 +# Thomas Rößl , 2016 +# Tobias Klepp , 2016 +# Torsten Karge , 2016 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" -"Last-Translator: Taiga Dev Team \n" +"PO-Revision-Date: 2016-06-24 07:58+0000\n" +"Last-Translator: Torsten Karge \n" "Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/de/)\n" "MIME-Version: 1.0\n" @@ -69,7 +72,7 @@ msgstr "Der Benutzer ist schon registriert." #: taiga/auth/services.py:146 msgid "This user is already a member of the project." -msgstr "" +msgstr "Dieser Benutzer ist schon ein Mitglied des Projektes." #: taiga/auth/services.py:172 msgid "Error on creating new user." @@ -220,7 +223,7 @@ msgstr "" #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 #: taiga/webhooks/api.py:68 msgid "Blocked element" -msgstr "" +msgstr "Blockiertes Element" #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." @@ -374,7 +377,7 @@ msgstr "Voraussetzungsfehler" #: taiga/base/exceptions.py:217 msgid "No room left for more projects." -msgstr "" +msgstr "Kein Raum für weitere Projekte." #: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." @@ -613,7 +616,7 @@ msgstr "Fehler beim Importieren der Chroniken" #: taiga/export_import/services/store.py:731 msgid "unexpected error importing project" -msgstr "" +msgstr "unerwarteter Fehler beim Projekt-Import" #: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" @@ -638,6 +641,21 @@ msgid "" "TRACE ERROR:\n" "------------" msgstr "" +"\n" +"\n" +"Fehler beim Laden des Dump von {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"GRUND:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"FEHLER-PFAD:\n" +"------------" #: taiga/export_import/tasks.py:110 msgid "Error loading project dump" @@ -645,11 +663,11 @@ msgstr "Fehler beim Laden von Projekt Export-Datei" #: taiga/export_import/tasks.py:111 msgid "Error loading your project dump file" -msgstr "" +msgstr "Fehler beim Laden Ihrer Projekt-Dump-Datei" #: taiga/export_import/tasks.py:125 msgid " -- no detail info --" -msgstr "" +msgstr "-- keine detaillierten Infos --" #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format @@ -1398,21 +1416,23 @@ msgstr "Ungültige Templatebeschreibung" #: taiga/projects/api.py:356 msgid "Invalid user id" -msgstr "" +msgstr "Ungültige Benutzer-Id" #: taiga/projects/api.py:362 msgid "The user doesn't exist" -msgstr "" +msgstr "Der Benutzer existiert nicht" #: taiga/projects/api.py:366 msgid "The user must be already a project member" -msgstr "" +msgstr "Der Benutzer muss bereits Mitglied des Projektes sein" #: taiga/projects/api.py:672 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" +"Das Projekt muss einen Eigentümer haben und mindestens ein Benutzer muss ein " +"aktiver Administrator sein" #: taiga/projects/api.py:706 msgid "You don't have permisions to see that." @@ -1501,11 +1521,11 @@ msgstr "" #: taiga/projects/choices.py:33 msgid "This project is blocked by admin staff" -msgstr "" +msgstr "Dieses Projekt ist durch den Administrator blockiert" #: taiga/projects/choices.py:34 msgid "This project is blocked because the owner left" -msgstr "" +msgstr "Dieses Projekt ist blockiert, weil es der Eigentümer verlassen hat." #: taiga/projects/custom_attributes/choices.py:27 msgid "Text" @@ -1521,7 +1541,7 @@ msgstr "Datum" #: taiga/projects/custom_attributes/choices.py:30 msgid "Url" -msgstr "" +msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 @@ -1856,7 +1876,7 @@ msgstr "voreingestellter Ticket Typ" #: taiga/projects/models.py:154 msgid "logo" -msgstr "" +msgstr "Logo" #: taiga/projects/models.py:164 msgid "members" @@ -1912,15 +1932,15 @@ msgstr "ist privat" #: taiga/projects/models.py:201 msgid "is featured" -msgstr "" +msgstr "ist gekennzeichnet" #: taiga/projects/models.py:204 msgid "is looking for people" -msgstr "" +msgstr "sucht nach Mitarbeitern" #: taiga/projects/models.py:206 msgid "loking for people note" -msgstr "" +msgstr "Hinweis für Mitarbeitersuche" #: taiga/projects/models.py:218 msgid "tags colors" @@ -1928,11 +1948,11 @@ msgstr "Tag Farben" #: taiga/projects/models.py:221 msgid "project transfer token" -msgstr "" +msgstr "Projekt-Transfer-Token" #: taiga/projects/models.py:225 msgid "blocked code" -msgstr "" +msgstr "Blockierter Code" #: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" @@ -1945,27 +1965,27 @@ msgstr "Count" #: taiga/projects/models.py:235 msgid "fans last week" -msgstr "" +msgstr "Unterstützer letzte Woche" #: taiga/projects/models.py:238 msgid "fans last month" -msgstr "" +msgstr "Unterstützer letzten Monat" #: taiga/projects/models.py:241 msgid "fans last year" -msgstr "" +msgstr "Unterstützer letztes Jahr" #: taiga/projects/models.py:247 msgid "activity last week" -msgstr "" +msgstr "Aktivitäten letzte Woche" #: taiga/projects/models.py:250 msgid "activity last month" -msgstr "" +msgstr "Aktivitäten letzten Monat" #: taiga/projects/models.py:253 msgid "activity last year" -msgstr "" +msgstr "Aktivitäten letztes Jahr" #: taiga/projects/models.py:467 msgid "modules config" @@ -2848,6 +2868,8 @@ msgstr "Version" msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" +"Sie können das Projekt nicht verlassen, wenn Sie der Eigentümer sind oder " +"wenn keine weiteren Administratoren vorhanden sind." #: taiga/projects/serializers.py:172 msgid "Email address is already taken" @@ -2859,11 +2881,12 @@ msgstr "Ungültige Rolle für dieses Projekt" #: taiga/projects/serializers.py:195 msgid "The project owner must be admin." -msgstr "" +msgstr "Der Projekteigentümer muss Administrator sein." #: taiga/projects/serializers.py:198 msgid "At least one user must be an active admin for this project." msgstr "" +"Mindestens ein Benutzer muss ein aktiver Administrator des Projektes sein." #: taiga/projects/serializers.py:396 msgid "Default options" @@ -2904,15 +2927,19 @@ msgstr "Rollen" #: taiga/projects/services/members.py:116 msgid "You have reached your current limit of memberships for private projects" msgstr "" +"Sie haben Ihr aktuelles Limit für die Mitgliederanzahl für private Projekte " +"erreicht" #: taiga/projects/services/members.py:120 msgid "You have reached your current limit of memberships for public projects" msgstr "" +"Sie haben Ihr aktuelles Limit für die Mitgliederanzahl für öffentliche " +"Projekte erreicht" #: taiga/projects/services/projects.py:69 #: taiga/projects/services/projects.py:106 taiga/users/services.py:582 msgid "You can't have more private projects" -msgstr "" +msgstr "Sie können nicht mehr private Projekte haben" #: taiga/projects/services/projects.py:73 #: taiga/projects/services/projects.py:110 taiga/users/services.py:585 @@ -2923,7 +2950,7 @@ msgstr "" #: taiga/projects/services/projects.py:77 #: taiga/projects/services/projects.py:114 taiga/users/services.py:589 msgid "You can't have more public projects" -msgstr "" +msgstr "Sie können nicht mehr öffentliche Projekte haben." #: taiga/projects/services/projects.py:81 #: taiga/projects/services/projects.py:118 taiga/users/services.py:592 @@ -2948,7 +2975,7 @@ msgstr "Token ist ungültig" #: taiga/projects/services/transfer.py:66 msgid "Token has expired" -msgstr "" +msgstr "Token ist abgelaufen" #: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." @@ -3175,7 +3202,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 #, python-format msgid "%(new_owner_name)s says:" -msgstr "" +msgstr "%(new_owner_name)s sagt:" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 msgid "" @@ -3191,6 +3218,8 @@ msgid "" "\n" "The Taiga Team\n" msgstr "" +"\n" +"Das Taiga-Team\n" #: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 #, python-format @@ -3283,7 +3312,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 #: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 msgid "Continue" -msgstr "" +msgstr "Fortsetzen" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 #, python-format @@ -3303,7 +3332,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 msgid "Go to your project settings:" -msgstr "" +msgstr "Geben Sie zu Ihren Projekt-Einstellungen:" #: taiga/projects/templates/emails/transfer_request-subject.jinja:1 #, python-format @@ -3329,6 +3358,8 @@ msgid "" "

%(owner_name)s says:

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

%(owner_name)s sagt:

" #: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 msgid "" @@ -3706,23 +3737,23 @@ msgstr "Prüfe die API der Historie auf Übereinstimmung" #: taiga/users/admin.py:38 msgid "Project Member" -msgstr "" +msgstr "Projektmitglied" #: taiga/users/admin.py:39 msgid "Project Members" -msgstr "" +msgstr "Projektmitglieder" #: taiga/users/admin.py:49 msgid "id" -msgstr "" +msgstr "Id" #: taiga/users/admin.py:81 msgid "Project Ownership" -msgstr "" +msgstr "Projekt-Besitz" #: taiga/users/admin.py:82 msgid "Project Ownerships" -msgstr "" +msgstr "Projekt-Besitze" #: taiga/users/admin.py:119 msgid "Personal info" @@ -3734,7 +3765,7 @@ msgstr "Berechtigungen" #: taiga/users/admin.py:123 msgid "Restrictions" -msgstr "" +msgstr "Einschränkungen" #: taiga/users/admin.py:125 msgid "Important dates" diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po index 2e440979..df5cf8cc 100644 --- a/taiga/locale/pt_BR/LC_MESSAGES/django.po +++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po @@ -3,6 +3,7 @@ # This file is distributed under the same license as the taiga-back package. # # Translators: +# Antônio "acdc" Jr. , 2016 # Cléber Zavadniak , 2015 # Thiago , 2015 # Daniel Dias , 2015 @@ -10,9 +11,11 @@ # Hevertton Barbosa , 2015 # Kemel Zaidan , 2015 # Lennon Jesus , 2016 +# Mairieli Wessel , 2016 # Marlon Carvalho , 2015 # pedromvm , 2015 # Renato Prado , 2015 +# Thiago Almeida , 2016 # Thiago , 2015 # Walker de Alencar , 2015 msgid "" @@ -20,8 +23,8 @@ msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" -"Last-Translator: Taiga Dev Team \n" +"PO-Revision-Date: 2016-06-13 01:32+0000\n" +"Last-Translator: Mairieli Wessel \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/pt_BR/)\n" "MIME-Version: 1.0\n" @@ -202,7 +205,7 @@ msgstr "" #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 #: taiga/webhooks/api.py:68 msgid "Blocked element" -msgstr "" +msgstr "Elemento bloqeado" #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." @@ -577,7 +580,7 @@ msgstr "erro importando sprints" #: taiga/export_import/services/store.py:683 msgid "error importing user stories" -msgstr "erro importando user stories" +msgstr "erro importando histórias de usuário" #: taiga/export_import/services/store.py:687 msgid "error importing tasks" @@ -585,7 +588,7 @@ msgstr "erro importando tarefas" #: taiga/export_import/services/store.py:691 msgid "error importing issues" -msgstr "erro importando casos" +msgstr "erro importando problemas" #: taiga/export_import/services/store.py:695 msgid "error importing wiki pages" @@ -605,7 +608,7 @@ msgstr "erro importando linha do tempo" #: taiga/export_import/services/store.py:731 msgid "unexpected error importing project" -msgstr "" +msgstr "erro inesperado ao importar projeto" #: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" @@ -630,6 +633,28 @@ msgid "" "TRACE ERROR:\n" "------------" msgstr "" +"\n" +"\n" +"\n" +"Erro ao carregar arquivo de restauração por {user_full_name} <{user_email}>:" +"\"\n" +"\n" +"\n" +"\n" +"\n" +"MOTIVO:\n" +"\n" +"-------\n" +"\n" +"{reason}\n" +"\n" +"\n" +"DETALHES:\n" +"--------\n" +"{details}\n" +"\n" +"MAIS INFORMAÇÕES DO ERRO:\n" +"------------" #: taiga/export_import/tasks.py:110 msgid "Error loading project dump" @@ -637,11 +662,11 @@ msgstr "Erro carregando arquivo de restauração do projeto" #: taiga/export_import/tasks.py:111 msgid "Error loading your project dump file" -msgstr "" +msgstr "Erro ao carregar arquivo de restauração do projeto" #: taiga/export_import/tasks.py:125 msgid " -- no detail info --" -msgstr "" +msgstr "-- sem informações detalhadas --" #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format @@ -1043,7 +1068,7 @@ msgstr "Status alterado em Bitbucket commit" #: taiga/hooks/bitbucket/event_hooks.py:124 #: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 msgid "Invalid issue information" -msgstr "Informação de caso inválida" +msgstr "Informação de problema inválida" #: taiga/hooks/bitbucket/event_hooks.py:140 #, python-brace-format @@ -1055,22 +1080,22 @@ msgid "" "\n" "{description}" msgstr "" -"Caso criado por [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " +"Problema criado por [@{bitbucket_user_name}]({bitbucket_user_url} \"Veja " +"profile do BitBucket de @{bitbucket_user_name}\") a partir do BitBucket.\n" +"Origem BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Ir para " "'bb#{number} - {subject}'\"):\n" "\n" "{description}" #: taiga/hooks/bitbucket/event_hooks.py:151 msgid "Issue created from BitBucket." -msgstr "Caso criado pelo Bitbucket." +msgstr "Problema criado via Bitbucket." #: taiga/hooks/bitbucket/event_hooks.py:175 #: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 #: taiga/hooks/gitlab/event_hooks.py:153 msgid "Invalid issue comment information" -msgstr "Informação de comentário de caso inválido" +msgstr "Informação de comentário de problema inválida" #: taiga/hooks/bitbucket/event_hooks.py:183 #, python-brace-format @@ -1125,7 +1150,7 @@ msgid "" "\n" "{description}" msgstr "" -"Caso criado por [@{github_user_name}]({github_user_url} \"See " +"Problema criado por [@{github_user_name}]({github_user_url} \"See " "@{github_user_name}'s GitHub profile\") from GitHub.\n" "Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " "'gh#{number} - {subject}'\"):\n" @@ -1134,7 +1159,7 @@ msgstr "" #: taiga/hooks/github/event_hooks.py:169 msgid "Issue created from GitHub." -msgstr "Caso criado pelo Github." +msgstr "Problema criado pelo Github." #: taiga/hooks/github/event_hooks.py:201 #, python-brace-format @@ -1212,7 +1237,7 @@ msgstr "Ver marco de progresso" #: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 msgid "View user stories" -msgstr "Ver user stories" +msgstr "Ver histórias de usuário" #: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 #: taiga/permissions/permissions.py:64 @@ -1222,7 +1247,7 @@ msgstr "Ver tarefa" #: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 #: taiga/permissions/permissions.py:69 msgid "View issues" -msgstr "Ver casos" +msgstr "Ver problemas" #: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 #: taiga/permissions/permissions.py:74 @@ -1240,11 +1265,11 @@ msgstr "Solicitar filiação" #: taiga/permissions/permissions.py:40 msgid "Add user story to project" -msgstr "Adicionar user story para projeto" +msgstr "Adicionar história de usuário em projeto" #: taiga/permissions/permissions.py:41 msgid "Add comments to user stories" -msgstr "Adicionar comentários para user story" +msgstr "Adicionar comentários em histórias de usuário" #: taiga/permissions/permissions.py:42 msgid "Add comments to tasks" @@ -1252,11 +1277,11 @@ msgstr "Adicionar comentário para tarefa" #: taiga/permissions/permissions.py:43 msgid "Add issues" -msgstr "Adicionar casos" +msgstr "Adicionar problemas" #: taiga/permissions/permissions.py:44 msgid "Add comments to issues" -msgstr "Adicionar comentários aos casos" +msgstr "Adicionar comentários aos problemas" #: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 msgid "Add wiki page" @@ -1288,19 +1313,19 @@ msgstr "Remover marco de progresso" #: taiga/permissions/permissions.py:59 msgid "View user story" -msgstr "Ver user story" +msgstr "Ver história de usuário" #: taiga/permissions/permissions.py:60 msgid "Add user story" -msgstr "Adicionar user story" +msgstr "Adicionar história de usuário" #: taiga/permissions/permissions.py:61 msgid "Modify user story" -msgstr "Modificar user story" +msgstr "Modificar história de usuário" #: taiga/permissions/permissions.py:62 msgid "Delete user story" -msgstr "Deletar user story" +msgstr "Apagar história de usuário" #: taiga/permissions/permissions.py:65 msgid "Add task" @@ -1316,15 +1341,15 @@ msgstr "Deletar tarefa" #: taiga/permissions/permissions.py:70 msgid "Add issue" -msgstr "Adicionar caso" +msgstr "Adicionar problema" #: taiga/permissions/permissions.py:71 msgid "Modify issue" -msgstr "Modificar caso" +msgstr "Modificar problema" #: taiga/permissions/permissions.py:72 msgid "Delete issue" -msgstr "Deletar caso" +msgstr "Deletar problema" #: taiga/permissions/permissions.py:77 msgid "Delete wiki page" @@ -1385,21 +1410,23 @@ msgstr "Descrição de template inválida" #: taiga/projects/api.py:356 msgid "Invalid user id" -msgstr "" +msgstr "Id de usuário inválido" #: taiga/projects/api.py:362 msgid "The user doesn't exist" -msgstr "" +msgstr "O usuário não existe" #: taiga/projects/api.py:366 msgid "The user must be already a project member" -msgstr "" +msgstr "O usuário deve ser um membro do projeto" #: taiga/projects/api.py:672 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" +"O projeto deve ter um dono e pelo menos um dos usuários precisa ser um " +"administrador ativo" #: taiga/projects/api.py:706 msgid "You don't have permisions to see that." @@ -1484,15 +1511,15 @@ msgstr "Talky" #: taiga/projects/choices.py:32 msgid "This project is blocked due to payment failure" -msgstr "" +msgstr "Este projeto está bloqueado por problemas de pagamento" #: taiga/projects/choices.py:33 msgid "This project is blocked by admin staff" -msgstr "" +msgstr "Este projeto está bloqueado por um administrador" #: taiga/projects/choices.py:34 msgid "This project is blocked because the owner left" -msgstr "" +msgstr "Este projeto está bloqueado porque o proprietário deixou o projeto" #: taiga/projects/custom_attributes/choices.py:27 msgid "Text" @@ -1508,7 +1535,7 @@ msgstr "Data" #: taiga/projects/custom_attributes/choices.py:30 msgid "Url" -msgstr "" +msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 @@ -1522,7 +1549,7 @@ msgstr "valores" #: taiga/projects/custom_attributes/models.py:98 #: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 msgid "user story" -msgstr "user story" +msgstr "história de usuário" #: taiga/projects/custom_attributes/models.py:113 msgid "task" @@ -1530,7 +1557,7 @@ msgstr "tarefa" #: taiga/projects/custom_attributes/models.py:128 msgid "issue" -msgstr "caso" +msgstr "problema" #: taiga/projects/custom_attributes/serializers.py:58 msgid "Already exists one with the same name." @@ -1671,23 +1698,24 @@ msgstr "sprint" #: taiga/projects/issues/api.py:158 msgid "You don't have permissions to set this sprint to this issue." -msgstr "Você não tem permissão para colocar esse sprint para esse caso." +msgstr "Você não tem permissão para colocar essa sprint para esse problema." #: taiga/projects/issues/api.py:162 msgid "You don't have permissions to set this status to this issue." -msgstr "Você não tem permissão para colocar esse status para esse caso." +msgstr "Você não tem permissão para colocar esse status para esse problema." #: taiga/projects/issues/api.py:166 msgid "You don't have permissions to set this severity to this issue." -msgstr "Você não tem permissão para colocar essa severidade para esse caso." +msgstr "Você não tem permissão para colocar essa gravidade para esse problema." #: taiga/projects/issues/api.py:170 msgid "You don't have permissions to set this priority to this issue." -msgstr "Você não tem permissão para colocar essa prioridade para esse caso." +msgstr "" +"Você não tem permissão para colocar essa prioridade para esse problema." #: taiga/projects/issues/api.py:174 msgid "You don't have permissions to set this type to this issue." -msgstr "Você não tem permissão para colocar esse tipo para esse caso." +msgstr "Você não tem permissão para colocar esse tipo para esse problema." #: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 #: taiga/projects/userstories/models.py:59 @@ -1831,15 +1859,15 @@ msgstr "severidade padrão" #: taiga/projects/models.py:134 msgid "default issue status" -msgstr "status padrão de caso" +msgstr "status padrão de problema" #: taiga/projects/models.py:138 msgid "default issue type" -msgstr "tipo padrão de caso" +msgstr "tipo padrão de problema" #: taiga/projects/models.py:154 msgid "logo" -msgstr "" +msgstr "logotipo" #: taiga/projects/models.py:164 msgid "members" @@ -1867,7 +1895,7 @@ msgstr "painel de wiki ativo" #: taiga/projects/models.py:177 taiga/projects/models.py:704 msgid "active issues panel" -msgstr "painel de casos ativo" +msgstr "painel de problemas ativo" #: taiga/projects/models.py:180 taiga/projects/models.py:707 msgid "videoconference system" @@ -1895,11 +1923,11 @@ msgstr "é privado" #: taiga/projects/models.py:201 msgid "is featured" -msgstr "" +msgstr "é destaque" #: taiga/projects/models.py:204 msgid "is looking for people" -msgstr "" +msgstr "está procurando colaboradores" #: taiga/projects/models.py:206 msgid "loking for people note" @@ -1940,15 +1968,15 @@ msgstr "" #: taiga/projects/models.py:247 msgid "activity last week" -msgstr "" +msgstr "atividades da última semana" #: taiga/projects/models.py:250 msgid "activity last month" -msgstr "" +msgstr "atividades do último mês" #: taiga/projects/models.py:253 msgid "activity last year" -msgstr "" +msgstr "atividades do último ano" #: taiga/projects/models.py:467 msgid "modules config" @@ -1996,11 +2024,11 @@ msgstr "status de tarefa" #: taiga/projects/models.py:715 msgid "issue statuses" -msgstr "status de casos" +msgstr "status de problemas" #: taiga/projects/models.py:716 msgid "issue types" -msgstr "tipos de caso" +msgstr "tipos de problema" #: taiga/projects/models.py:717 msgid "priorities" @@ -2016,15 +2044,15 @@ msgstr "funções" #: taiga/projects/notifications/choices.py:29 msgid "Involved" -msgstr "" +msgstr "Envolvido" #: taiga/projects/notifications/choices.py:30 msgid "All" -msgstr "" +msgstr "Tudo" #: taiga/projects/notifications/choices.py:31 msgid "None" -msgstr "" +msgstr "Nada" #: taiga/projects/notifications/models.py:63 msgid "created date time" @@ -2065,11 +2093,12 @@ msgid "" " " msgstr "" "\n" -"

Caso atualizado

\n" -"

Olá %(user)s,
%(changer)s atualizou caso em %(project)s

\n" -"

Caso #%(ref)s %(subject)s

\n" -" Ver caso\n" +"

Problema atualizado

\n" +"

Olá %(user)s,
%(changer)s atualizou um problema em %(project)s\n" +"

Problema #%(ref)s %(subject)s

\n" +" Ver problema\n" "\n" " " @@ -2082,9 +2111,9 @@ msgid "" "See issue #%(ref)s %(subject)s at %(url)s\n" msgstr "" "\n" -"Caso atualizado\n" -"Olá %(user)s, %(changer)s atualizou um caso em %(project)s\n" -"Ver caso #%(ref)s %(subject)s em %(url)s\n" +"Problema atualizado\n" +"Olá %(user)s, %(changer)s atualizou um problema em %(project)s\n" +"Ver problema #%(ref)s %(subject)s em %(url)s\n" #: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1 #, python-format @@ -2093,7 +2122,7 @@ msgid "" "[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n" msgstr "" "\n" -"[%(project)s] Atualizou o caso #%(ref)s \"%(subject)s\"\n" +"[%(project)s] Atualização do problema #%(ref)s \"%(subject)s\"\n" #: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4 #, python-format @@ -2109,12 +2138,13 @@ msgid "" " " msgstr "" "\n" -"

Novo caso criado

\n" -"

Olá %(user)s,
%(changer)s criou um novo caso em %(project)s

\n" -"

Caso #%(ref)s %(subject)s

\n" -" Ver caso\n" -"

O Time Taiga

\n" +"

Novo problema criado

\n" +"

Olá %(user)s,
%(changer)s criou um novo problema em %(project)s\n" +"

Problema #%(ref)s %(subject)s

\n" +" Ver problema\n" +"

Time Taiga

\n" " " #: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1 @@ -2129,12 +2159,12 @@ msgid "" "The Taiga Team\n" msgstr "" "\n" -"Novo caso criado\n" -"Olá %(user)s, %(changer)s criou um novo caso em %(project)s\n" -"Ver caso #%(ref)s %(subject)s em %(url)s\n" +"Novo problema criado\n" +"Olá %(user)s, %(changer)s criou um novo problema em %(project)s\n" +"Ver problema #%(ref)s %(subject)s em %(url)s\n" "\n" "---\n" -"O Time Taiga\n" +"Time Taiga\n" #: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1 #, python-format @@ -2143,7 +2173,7 @@ msgid "" "[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n" msgstr "" "\n" -"[%(project)s] Criou o caso #%(ref)s \"%(subject)s\"\n" +"[%(project)s] Criação do problema #%(ref)s \"%(subject)s\"\n" #: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4 #, python-format @@ -2157,10 +2187,10 @@ msgid "" " " msgstr "" "\n" -"

Caso apagado

\n" -"

Olá %(user)s,
%(changer)s apagou um caso em %(project)s

\n" -"

Caso #%(ref)s %(subject)s

\n" -"

O Time Taiga

\n" +"

Problema apagado

\n" +"

Olá %(user)s,
%(changer)s apagou um problema em %(project)s

\n" +"

Problema #%(ref)s %(subject)s

\n" +"

Time Taiga

\n" " " #: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1 @@ -2175,12 +2205,12 @@ msgid "" "The Taiga Team\n" msgstr "" "\n" -"Caso apagado\n" -"Olá %(user)s, %(changer)s apagou um caso em %(project)s\n" -"caso #%(ref)s %(subject)s\n" +"Problema apagado\n" +"Olá %(user)s, %(changer)s apagou um problema em %(project)s\n" +"Problema #%(ref)s %(subject)s\n" "\n" "---\n" -"O Time Taiga\n" +"Time Taiga\n" #: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1 #, python-format @@ -2189,7 +2219,7 @@ msgid "" "[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n" msgstr "" "\n" -"[%(project)s] Apagou o caso #%(ref)s \"%(subject)s\"\n" +"[%(project)s] Apagado o problema #%(ref)s \"%(subject)s\"\n" #: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4 #, python-format @@ -2204,8 +2234,8 @@ msgid "" " " msgstr "" "\n" -"

Sprint atualizado

\n" -"

Olá %(user)s,
%(changer)s atualizou um sprint em %(project)sSprint atualizada\n" +"

Olá %(user)s,
%(changer)s atualizou uma sprint em %(project)s\n" "

Sprint %(name)s

\n" " User Story atualizada\n" -"

Olá %(user)s,
%(changer)s atualizou a user story em %(project)s\n" -"

User Story #%(ref)s %(subject)s

\n" -"
Ver user story\n" +"

História de Usuário atualizada

\n" +"

Olá %(user)s,
%(changer)s atualizou a história de usuário em " +"%(project)s

\n" +"

História de Usuário #%(ref)s %(subject)s

\n" +" Ver hstória de usuário\n" " " #: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3 @@ -2498,9 +2528,9 @@ msgid "" "See user story #%(ref)s %(subject)s at %(url)s\n" msgstr "" "\n" -"User story atualizada\n" -"Olá %(user)s, %(changer)s atualizou a user story em %(project)s\n" -"Ver user story #%(ref)s %(subject)s em %(url)s\n" +"História de usuário atualizada\n" +"Olá %(user)s, %(changer)s atualizou a história de usuário em %(project)s\n" +"Ver história de usuário #%(ref)s %(subject)s em %(url)s\n" #: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1 #, python-format @@ -2509,7 +2539,7 @@ msgid "" "[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n" msgstr "" "\n" -"[%(project)s] Atualizou a US #%(ref)s \"%(subject)s\"\n" +"[%(project)s] Atualização da História de Usuário #%(ref)s \"%(subject)s\"\n" #: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4 #, python-format @@ -2525,13 +2555,13 @@ msgid "" " " msgstr "" "\n" -"

Nova user story criada

\n" -"

Olá %(user)s,
%(changer)s criou nova user story em %(project)s\n" -"

User Story #%(ref)s %(subject)s

\n" -" Ver user story\n" -"

O Time Taiga

\n" +"

Nova história de usuário criada

\n" +"

Olá %(user)s,
%(changer)s criou nova história de usuário em " +"%(project)s

\n" +"

História de Usuário #%(ref)s %(subject)s

\n" +" Ver história de usuário\n" +"

Time Taiga

\n" " " #: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1 @@ -2546,12 +2576,12 @@ msgid "" "The Taiga Team\n" msgstr "" "\n" -"Nova user story criada\n" -"Olá %(user)s, %(changer)s criou nova user story em %(project)s\n" -"Ver user story #%(ref)s %(subject)s em %(url)s\n" +"Nova história de usuário criada\n" +"Olá %(user)s, %(changer)s criou nova história de usuário em %(project)s\n" +"Ver história de usuário #%(ref)s %(subject)s em %(url)s\n" "\n" "---\n" -"O Time Taiga\n" +"Time Taiga\n" #: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1 #, python-format @@ -2574,11 +2604,11 @@ msgid "" " " msgstr "" "\n" -"

User Story apagada

\n" -"

Olá %(user)s,
%(changer)s apagou uma user story em %(project)s\n" -"

User Story #%(ref)s %(subject)s

\n" -"

O Time Taiga

\n" +"

História de Usuário apagada

\n" +"

Olá %(user)s,
%(changer)s apagou uma história de usuário em " +"%(project)s

\n" +"

História de Usuário #%(ref)s %(subject)s

\n" +"

Time Taiga

\n" " " #: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1 @@ -2593,12 +2623,12 @@ msgid "" "The Taiga Team\n" msgstr "" "\n" -"User Story apagada\n" -"Olá %(user)s, %(changer)s apagou user story em %(project)s\n" -"User Story #%(ref)s %(subject)s\n" +"História de Usuário apagada\n" +"Olá %(user)s, %(changer)s apagou história de usuário em %(project)s\n" +"História de Usuário #%(ref)s %(subject)s\n" "\n" "---\n" -"O Time Taiga\n" +"Time Taiga\n" #: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1 #, python-format @@ -2654,7 +2684,7 @@ msgid "" "[%(project)s] Updated the Wiki Page \"%(page)s\"\n" msgstr "" "\n" -"[%(project)s] Atualizou a página wiki \"%(page)s\"\n" +"[%(project)s] Atualização da página wiki \"%(page)s\"\n" #: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-html.jinja:4 #, python-format @@ -2786,6 +2816,8 @@ msgstr "versão" msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" +"Você não pode deixar o projeto se você é o dono é não há outros " +"administradores" #: taiga/projects/serializers.py:172 msgid "Email address is already taken" @@ -2797,11 +2829,12 @@ msgstr "Função inválida para projeto" #: taiga/projects/serializers.py:195 msgid "The project owner must be admin." -msgstr "" +msgstr "O dono do projeto deve ser um administrador." #: taiga/projects/serializers.py:198 msgid "At least one user must be an active admin for this project." msgstr "" +"Pelo menos one dos usuários deve ser um administrador ativo neste projeto." #: taiga/projects/serializers.py:396 msgid "Default options" @@ -2809,7 +2842,7 @@ msgstr "Opções padrão" #: taiga/projects/serializers.py:397 msgid "User story's statuses" -msgstr "Status de user story" +msgstr "Status de história de usuário" #: taiga/projects/serializers.py:398 msgid "Points" @@ -2821,11 +2854,11 @@ msgstr "Status de tarefas" #: taiga/projects/serializers.py:400 msgid "Issue's statuses" -msgstr "Status de casos" +msgstr "Status de problemas" #: taiga/projects/serializers.py:401 msgid "Issue's types" -msgstr "Tipos de casos" +msgstr "Tipos de problemas" #: taiga/projects/serializers.py:402 msgid "Priorities" @@ -2841,33 +2874,35 @@ msgstr "Funções" #: taiga/projects/services/members.py:116 msgid "You have reached your current limit of memberships for private projects" -msgstr "" +msgstr "Você atingiu o seu limite atual de membros para projetos privados" #: taiga/projects/services/members.py:120 msgid "You have reached your current limit of memberships for public projects" -msgstr "" +msgstr "Você atingiu o seu limite atual de membros para projetos públicos" #: taiga/projects/services/projects.py:69 #: taiga/projects/services/projects.py:106 taiga/users/services.py:582 msgid "You can't have more private projects" -msgstr "" +msgstr "Você não pode ter mais projetos privados" #: taiga/projects/services/projects.py:73 #: taiga/projects/services/projects.py:110 taiga/users/services.py:585 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" +"Este projeto atingiu o seu limite atual de membros para projetos privados" #: taiga/projects/services/projects.py:77 #: taiga/projects/services/projects.py:114 taiga/users/services.py:589 msgid "You can't have more public projects" -msgstr "" +msgstr "Você não pode ter mais projetos públicos" #: taiga/projects/services/projects.py:81 #: taiga/projects/services/projects.py:118 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" +"Este projeto atingiu o seu limite atual de membros para projetos públicos" #: taiga/projects/services/stats.py:196 msgid "Future sprint" @@ -2886,7 +2921,7 @@ msgstr "Token é inválido" #: taiga/projects/services/transfer.py:66 msgid "Token has expired" -msgstr "" +msgstr "Token expirou" #: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." @@ -2894,7 +2929,9 @@ msgstr "Você não tem permissão para colocar esse sprint para essa tarefa." #: taiga/projects/tasks/api.py:116 msgid "You don't have permissions to set this user story to this task." -msgstr "Você não tem permissão para colocar essa user story para essa tarefa." +msgstr "" +"Você não tem permissão para colocar essa história de usuário para essa " +"tarefa." #: taiga/projects/tasks/api.py:119 msgid "You don't have permissions to set this status to this task." @@ -3074,11 +3111,15 @@ msgid "" "new project owner for \"%(project_name)s\".

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

Olá %(old_owner_name)s,

\n" +"

%(new_owner_name)s aceitou sua oferta e será o novo dono do projeto " +"\"%(project_name)s\".

" #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 #, python-format msgid "

%(new_owner_name)s says:

" -msgstr "" +msgstr "

%(new_owner_name)s diz:

" #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 msgid "" @@ -3100,7 +3141,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 #, python-format msgid "%(new_owner_name)s says:" -msgstr "" +msgstr "%(new_owner_name)s diz:" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 msgid "" @@ -3116,6 +3157,8 @@ msgid "" "\n" "The Taiga Team\n" msgstr "" +"\n" +"Time Taiga\n" #: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 #, python-format @@ -3123,6 +3166,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer offer accepted!\n" msgstr "" +"\n" +"[%(project)s] Oferta de transferência de propriedade de projeto aceita!\n" #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 #, python-format @@ -3141,6 +3186,9 @@ msgid "" "

%(rejecter_name)s says:

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

%(rejecter_name)s diz:

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 msgid "" @@ -3167,7 +3215,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 #, python-format msgid "%(rejecter_name)s says:" -msgstr "" +msgstr "%(rejecter_name)s diz:" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 msgid "" @@ -3208,7 +3256,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 #: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 msgid "Continue" -msgstr "" +msgstr "Continuar" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 #, python-format @@ -3275,7 +3323,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 #, python-format msgid "%(owner_name)s says:" -msgstr "" +msgstr "%(owner_name)s diz:" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 msgid "" @@ -3539,16 +3587,20 @@ msgstr "Stakeholder" #: taiga/projects/userstories/api.py:163 msgid "You don't have permissions to set this sprint to this user story." -msgstr "Você não tem permissão para colocar esse sprint para essa user story." +msgstr "" +"Você não tem permissão para colocar esse sprint para essa história de " +"usuário." #: taiga/projects/userstories/api.py:167 msgid "You don't have permissions to set this status to this user story." -msgstr "Você não tem permissão para colocar esse status para essa user story." +msgstr "" +"Você não tem permissão para colocar esse status para essa história de " +"usuário." #: taiga/projects/userstories/api.py:267 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" -msgstr "" +msgstr "Gerando a história de usuário #{ref} - {subject}" #: taiga/projects/userstories/models.py:39 msgid "role" @@ -3577,11 +3629,11 @@ msgstr "É requerimento do time" #: taiga/projects/userstories/models.py:104 msgid "generated from issue" -msgstr "Gerado do caso" +msgstr "Gerado do problema" #: taiga/projects/userstories/validators.py:29 msgid "There's no user story with that id" -msgstr "Não há user story com esse id" +msgstr "Não há história de usuário com esse id" #: taiga/projects/validators.py:29 msgid "There's no project with that id" @@ -3589,7 +3641,7 @@ msgstr "Não há projeto com esse id" #: taiga/projects/validators.py:38 msgid "There's no user story status with that id" -msgstr "Não há status de user story com este id" +msgstr "Não há status de história de usuário com este id" #: taiga/projects/validators.py:47 msgid "There's no task status with that id" @@ -3626,15 +3678,15 @@ msgstr "Verifique o histórico da API para a exata diferença" #: taiga/users/admin.py:38 msgid "Project Member" -msgstr "" +msgstr "Membro do Projeto" #: taiga/users/admin.py:39 msgid "Project Members" -msgstr "" +msgstr "Membros do Projeto" #: taiga/users/admin.py:49 msgid "id" -msgstr "" +msgstr "id" #: taiga/users/admin.py:81 msgid "Project Ownership" @@ -3654,7 +3706,7 @@ msgstr "Permissões" #: taiga/users/admin.py:123 msgid "Restrictions" -msgstr "" +msgstr "Restrições" #: taiga/users/admin.py:125 msgid "Important dates" @@ -3850,7 +3902,7 @@ msgstr "" "Você pode ignorar essa mensagem caso não tenha solicitado\n" "\n" "---\n" -"O Time Taiga\n" +"Time Taiga\n" #: taiga/users/templates/emails/change_email-subject.jinja:1 msgid "[Taiga] Change email" From 1252a08dbbb2e74d41210cb536c1747f8bc3df7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 6 Jul 2016 10:27:27 +0200 Subject: [PATCH 082/261] Addd flake8 config file --- setup.cfg | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2bd4593c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[flake8] +ignore = E41,E266 +max-line-length = 120 +exclude = + .git, + *__pycache__*, + *tests*, + *scripts*, + *migrations*, + *management* +max-complexity = 10 From 78a2118e8e08eebcabde02d6e12584dacf0d1a3f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 23 Jun 2016 15:11:16 +0200 Subject: [PATCH 083/261] API performance --- taiga/base/api/serializers.py | 2 + taiga/permissions/services.py | 70 +-- taiga/projects/api.py | 50 +- taiga/projects/filters.py | 35 +- taiga/projects/issues/api.py | 1 - taiga/projects/notifications/mixins.py | 7 +- taiga/projects/serializers.py | 283 ++++++++++-- taiga/projects/services/projects.py | 53 ++- taiga/projects/utils.py | 436 ++++++++++++++++++ taiga/users/models.py | 2 +- .../test_projects_resource.py | 6 +- tests/integration/test_projects.py | 2 +- 12 files changed, 818 insertions(+), 129 deletions(-) create mode 100644 taiga/projects/utils.py diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 7de82458..601c1753 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -1228,4 +1228,6 @@ class LightSerializer(serpy.Serializer): kwargs.pop("read_only", None) kwargs.pop("partial", None) kwargs.pop("files", None) + context = kwargs.pop("context", {}) super().__init__(*args, **kwargs) + self.context = context diff --git a/taiga/permissions/services.py b/taiga/permissions/services.py index 32357926..6d89c168 100644 --- a/taiga/permissions/services.py +++ b/taiga/permissions/services.py @@ -91,39 +91,55 @@ def _get_membership_permissions(membership): return [] +def calculate_permissions(is_authenticated=False, is_superuser=False, is_member=False, + is_admin=False, role_permissions=[], anon_permissions=[], + public_permissions=[]): + if is_superuser: + admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) + members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) + public_permissions = [] + anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) + elif is_member: + if is_admin: + admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) + members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) + else: + admins_permissions = [] + members_permissions = [] + members_permissions = members_permissions + role_permissions + public_permissions = public_permissions if public_permissions is not None else [] + anon_permissions = anon_permissions if anon_permissions is not None else [] + elif is_authenticated: + admins_permissions = [] + members_permissions = [] + public_permissions = public_permissions if public_permissions is not None else [] + anon_permissions = anon_permissions if anon_permissions is not None else [] + else: + admins_permissions = [] + members_permissions = [] + public_permissions = [] + anon_permissions = anon_permissions if anon_permissions is not None else [] + + return set(admins_permissions + members_permissions + public_permissions + anon_permissions) + + def get_user_project_permissions(user, project, cache="user"): """ cache param determines how memberships are calculated trying to reuse the existing data in cache """ membership = _get_user_project_membership(user, project, cache=cache) - if user.is_superuser: - admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) - members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) - public_permissions = [] - anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) - elif membership: - if membership.is_admin: - admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) - members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) - else: - admins_permissions = [] - members_permissions = [] - members_permissions = members_permissions + _get_membership_permissions(membership) - public_permissions = project.public_permissions if project.public_permissions is not None else [] - anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] - elif user.is_authenticated(): - admins_permissions = [] - members_permissions = [] - public_permissions = project.public_permissions if project.public_permissions is not None else [] - anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] - else: - admins_permissions = [] - members_permissions = [] - public_permissions = [] - anon_permissions = project.anon_permissions if project.anon_permissions is not None else [] - - return set(admins_permissions + members_permissions + public_permissions + anon_permissions) + is_member = membership is not None + is_admin = is_member and membership.is_admin + return calculate_permissions( + is_authenticated = user.is_authenticated(), + is_superuser = user.is_superuser, + is_member = is_member, + is_admin = is_admin, + role_permissions = _get_membership_permissions(membership), + anon_permissions = project.anon_permissions, + public_permissions = project.public_permissions + ) def set_base_permissions_for_project(project): diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 6c7a4ec9..c6fbbe0d 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -61,7 +61,7 @@ from . import models from . import permissions from . import serializers from . import services - +from . import utils as project_utils ###################################################### ## Project @@ -70,11 +70,9 @@ from . import services class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin, TagsColorsResourceMixin, ModelCrudViewSet): queryset = models.Project.objects.all() - serializer_class = serializers.ProjectDetailSerializer - admin_serializer_class = serializers.ProjectDetailAdminSerializer - list_serializer_class = serializers.ProjectSerializer permission_classes = (permissions.ProjectPermission, ) - filter_backends = (project_filters.QFilterBackend, + filter_backends = (project_filters.UserOrderFilterBackend, + project_filters.QFilterBackend, project_filters.CanViewProjectObjFilterBackend, project_filters.DiscoverModeFilterBackend) @@ -85,8 +83,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix "is_kanban_activated") ordering = ("name", "id") - order_by_fields = ("memberships__user_order", - "total_fans", + order_by_fields = ("total_fans", "total_fans_last_week", "total_fans_last_month", "total_fans_last_year", @@ -106,18 +103,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix def get_queryset(self): qs = super().get_queryset() - qs = qs.select_related("owner") - # Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries) - # so we add some custom prefetching - qs = qs.prefetch_related("members") - qs = qs.prefetch_related("memberships") - qs = qs.prefetch_related(Prefetch("notify_policies", - NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies")) - - Milestone = apps.get_model("milestones", "Milestone") - qs = qs.prefetch_related(Prefetch("milestones", - Milestone.objects.filter(closed=True), to_attr="closed_milestones")) + qs = project_utils.attach_extra_info(qs, user=self.request.user) # If filtering an activity period we must exclude the activities not updated recently enough now = timezone.now() @@ -137,22 +124,20 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix return qs + def retrieve(self, request, *args, **kwargs): + if self.action == "by_slug": + self.lookup_field = "slug" + + return super().retrieve(request, *args, **kwargs) + def get_serializer_class(self): - serializer_class = self.serializer_class - if self.action == "list": - serializer_class = self.list_serializer_class - elif self.action != "create": - if self.action == "by_slug": - slug = self.request.QUERY_PARAMS.get("slug", None) - project = get_object_or_404(models.Project, slug=slug) - else: - project = self.get_object() + return serializers.LightProjectSerializer - if permissions_services.is_project_admin(self.request.user, project): - serializer_class = self.admin_serializer_class + if self.action in ["retrieve", "by_slug"]: + return serializers.LightProjectDetailSerializer - return serializer_class + return serializers.ProjectSerializer @detail_route(methods=["POST"]) def change_logo(self, request, *args, **kwargs): @@ -283,10 +268,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix return response.Ok(data) @list_route(methods=["GET"]) - def by_slug(self, request): + def by_slug(self, request, *args, **kwargs): slug = request.QUERY_PARAMS.get("slug", None) - project = get_object_or_404(models.Project, slug=slug) - return self.retrieve(request, pk=project.pk) + return self.retrieve(request, slug=slug) @detail_route(methods=["GET", "PATCH"]) def modules(self, request, pk=None): diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py index b3be1a0a..cbb692b8 100644 --- a/taiga/projects/filters.py +++ b/taiga/projects/filters.py @@ -45,7 +45,7 @@ class DiscoverModeFilterBackend(FilterBackend): if request.QUERY_PARAMS.get("is_featured", None) == 'true': qs = qs.order_by("?") - return super().filter_queryset(request, qs.distinct(), view) + return super().filter_queryset(request, qs, view) class CanViewProjectObjFilterBackend(FilterBackend): @@ -86,7 +86,7 @@ class CanViewProjectObjFilterBackend(FilterBackend): # external users / anonymous qs = qs.filter(anon_permissions__contains=["view_project"]) - return super().filter_queryset(request, qs.distinct(), view) + return super().filter_queryset(request, qs, view) class QFilterBackend(FilterBackend): @@ -121,3 +121,34 @@ class QFilterBackend(FilterBackend): params=params, order_by=order_by) return queryset + + +class UserOrderFilterBackend(FilterBackend): + def filter_queryset(self, request, queryset, view): + if request.user.is_anonymous(): + return queryset + + raw_fieldname = request.QUERY_PARAMS.get(self.order_by_query_param, None) + if not raw_fieldname: + return queryset + + if raw_fieldname.startswith("-"): + field_name = raw_fieldname[1:] + else: + field_name = raw_fieldname + + if field_name != "user_order": + return queryset + + model = queryset.model + sql = """SELECT projects_membership.user_order + FROM projects_membership + WHERE + projects_membership.project_id = {tbl}.id AND + projects_membership.user_id = {user_id} + """ + + sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id) + queryset = queryset.extra(select={"user_order": sql}) + queryset = queryset.order_by(raw_fieldname) + return queryset diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 57acfca8..8da13476 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -144,7 +144,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W def get_queryset(self): qs = super().get_queryset() - qs = qs.prefetch_related("attachments", "generated_user_stories") qs = qs.select_related("owner", "assigned_to", "status", "project") qs = self.attach_votes_attrs_to_queryset(qs) return self.attach_watchers_attrs_to_queryset(qs) diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 62db374e..2cad1e97 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -188,9 +188,10 @@ class WatchedModelMixin(object): class BaseWatchedResourceModelSerializer(object): def get_is_watcher(self, obj): + # The "is_watcher" attribute is attached in the get_queryset of the viewset. if "request" in self.context: user = self.context["request"].user - return user.is_authenticated() and user.is_watcher(obj) + return user.is_authenticated() and getattr(obj, "is_watcher", False) return False @@ -205,8 +206,8 @@ class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, seriali class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer): - is_watcher = serializers.SerializerMethodField("get_is_watcher") - total_watchers = serializers.SerializerMethodField("get_total_watchers") + is_watcher = serpy.MethodField("get_is_watcher") + total_watchers = serpy.MethodField("get_total_watchers") class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 9c185a97..c10a4810 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -25,12 +25,15 @@ from taiga.base.api import serializers from taiga.base.fields import JsonField from taiga.base.fields import PgArrayField +from taiga.permissions import services as permissions_services from taiga.users.services import get_photo_or_gravatar_url from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import ProjectRoleSerializer +from taiga.users.serializers import ListUserBasicInfoSerializer from taiga.users.validators import RoleExistsValidator from taiga.permissions.services import get_user_project_permissions +from taiga.permissions.services import calculate_permissions from taiga.permissions.services import is_project_admin, is_project_owner from . import models @@ -46,6 +49,7 @@ from .tagging.fields import TagsField from .tagging.fields import TagsColorsField from .validators import ProjectExistsValidator +import serpy ###################################################### ## Custom values for selectors @@ -295,11 +299,6 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ return False def get_total_closed_milestones(self, obj): - # The "closed_milestone" attribute can be attached in the get_queryset method of the viewset. - qs_closed_milestones = getattr(obj, "closed_milestones", None) - if qs_closed_milestones is not None: - return len(qs_closed_milestones) - return obj.milestones.filter(closed=True).count() def get_notify_level(self, obj): @@ -310,11 +309,6 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ return None def get_total_watchers(self, obj): - # The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset. - qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None) - if qs_valid_notify_policies is not None: - return len(qs_valid_notify_policies) - return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count() def get_logo_small_url(self, obj): @@ -324,60 +318,253 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ return services.get_logo_big_thumbnail_url(obj) -class ProjectDetailSerializer(ProjectSerializer): - us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories - points = PointsSerializer(many=True, required=False) +class LightProjectSerializer(serializers.LightSerializer): + id = serpy.Field() + name = serpy.Field() + slug = serpy.Field() + description = serpy.Field() + created_date = serpy.Field() + modified_date = serpy.Field() + owner = serpy.MethodField() + members = serpy.MethodField() + total_milestones = serpy.Field() + total_story_points = serpy.Field() + is_backlog_activated = serpy.Field() + is_kanban_activated = serpy.Field() + is_wiki_activated = serpy.Field() + is_issues_activated = serpy.Field() + videoconferences = serpy.Field() + videoconferences_extra_data = serpy.Field() + creation_template = serpy.Field(attr="creation_template_id") + is_private = serpy.Field() + anon_permissions = serpy.Field() + public_permissions = serpy.Field() + is_featured = serpy.Field() + is_looking_for_people = serpy.Field() + looking_for_people_note = serpy.Field() + blocked_code = serpy.Field() + totals_updated_datetime = serpy.Field() + total_fans = serpy.Field() + total_fans_last_week = serpy.Field() + total_fans_last_month = serpy.Field() + total_fans_last_year = serpy.Field() + total_activity = serpy.Field() + total_activity_last_week = serpy.Field() + total_activity_last_month = serpy.Field() + total_activity_last_year = serpy.Field() - task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks + tags = serpy.Field() + tags_colors = serpy.MethodField() - issue_statuses = IssueStatusSerializer(many=True, required=False) - issue_types = IssueTypeSerializer(many=True, required=False) - priorities = PrioritySerializer(many=True, required=False) # Issues - severities = SeveritySerializer(many=True, required=False) + default_points = serpy.Field(attr="default_points_id") + default_us_status = serpy.Field(attr="default_us_status_id") + default_task_status = serpy.Field(attr="default_task_status_id") + default_priority = serpy.Field(attr="default_priority_id") + default_severity = serpy.Field(attr="default_severity_id") + default_issue_status = serpy.Field(attr="default_issue_status_id") + default_issue_type = serpy.Field(attr="default_issue_type_id") - userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes", - many=True, required=False) - task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes", - many=True, required=False) - issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes", - many=True, required=False) + my_permissions = serpy.MethodField() - roles = ProjectRoleSerializer(source="roles", many=True, read_only=True) - members = serializers.SerializerMethodField(method_name="get_members") - total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships") - is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits") + i_am_owner = serpy.MethodField() + i_am_admin = serpy.MethodField() + i_am_member = serpy.MethodField() + + notify_level = serpy.MethodField("get_notify_level") + total_closed_milestones = serpy.MethodField() + + is_watcher = serpy.MethodField() + total_watchers = serpy.MethodField() + + logo_small_url = serpy.MethodField() + logo_big_url = serpy.MethodField() + + is_fan = serpy.Field(attr="is_fan_attr") def get_members(self, obj): - qs = obj.memberships.filter(user__isnull=False) - qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"}) - qs = qs.order_by("complete_user_name") - qs = qs.select_related("role", "user") - serializer = ProjectMemberSerializer(qs, many=True) - return serializer.data + assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute" + if obj.members_attr is None: + return [] + + return [m.get("id") for m in obj.members_attr if m["id"] is not None] + + def get_i_am_member(self, obj): + assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute" + if obj.members_attr is None: + return False + + if "request" in self.context: + user = self.context["request"].user + if not user.is_anonymous() and user.id in [m.get("id") for m in obj.members_attr if m["id"] is not None]: + return True + + return False + + def get_tags_colors(self, obj): + return dict(obj.tags_colors) + + def get_my_permissions(self, obj): + if "request" in self.context: + user = self.context["request"].user + return calculate_permissions( + is_authenticated = user.is_authenticated(), + is_superuser = user.is_superuser, + is_member = self.get_i_am_member(obj), + is_admin = self.get_i_am_admin(obj), + role_permissions = obj.my_role_permissions_attr, + anon_permissions = obj.anon_permissions, + public_permissions = obj.public_permissions) + return [] + + def get_owner(self, obj): + return ListUserBasicInfoSerializer(obj.owner).data + + def get_i_am_owner(self, obj): + if "request" in self.context: + return is_project_owner(self.context["request"].user, obj) + return False + + def get_i_am_admin(self, obj): + if "request" in self.context: + return is_project_admin(self.context["request"].user, obj) + return False + + def get_total_closed_milestones(self, obj): + assert hasattr(obj, "closed_milestones_attr"), "instance must have a closed_milestones_attr attribute" + return obj.closed_milestones_attr + + def get_is_watcher(self, obj): + assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute" + np = self.get_notify_level(obj) + return np != None and np != NotifyLevel.none + + def get_total_watchers(self, obj): + assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute" + if obj.notify_policies_attr is None: + return 0 + + valid_notify_policies = [np for np in obj.notify_policies_attr if np["notify_level"] != NotifyLevel.none] + return len(valid_notify_policies) + + def get_notify_level(self, obj): + assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute" + if obj.notify_policies_attr is None: + return None + + if "request" in self.context: + user = self.context["request"].user + for np in obj.notify_policies_attr: + if np["user_id"] == user.id: + return np["notify_level"] + + return None + + def get_logo_small_url(self, obj): + return services.get_logo_small_thumbnail_url(obj) + + def get_logo_big_url(self, obj): + return services.get_logo_big_thumbnail_url(obj) + + +class LightProjectDetailSerializer(LightProjectSerializer): + us_statuses = serpy.Field(attr="userstory_statuses_attr") + points = serpy.Field(attr="points_attr") + task_statuses = serpy.Field(attr="task_statuses_attr") + issue_statuses = serpy.Field(attr="issue_statuses_attr") + issue_types = serpy.Field(attr="issue_types_attr") + priorities = serpy.Field(attr="priorities_attr") + severities = serpy.Field(attr="severities_attr") + userstory_custom_attributes = serpy.Field(attr="userstory_custom_attributes_attr") + task_custom_attributes = serpy.Field(attr="task_custom_attributes_attr") + issue_custom_attributes = serpy.Field(attr="issue_custom_attributes_attr") + roles = serpy.Field(attr="roles_attr") + members = serpy.MethodField() + total_memberships = serpy.MethodField() + is_out_of_owner_limits = serpy.MethodField() + + #Admin fields + is_private_extra_info = serpy.MethodField() + max_memberships = serpy.MethodField() + issues_csv_uuid = serpy.Field() + tasks_csv_uuid = serpy.Field() + userstories_csv_uuid = serpy.Field() + transfer_token = serpy.Field() + + def to_value(self, instance): + # Name attributes must be translated + for attr in ["userstory_statuses_attr","points_attr", "task_statuses_attr", + "issue_statuses_attr", "issue_types_attr", "priorities_attr", + "severities_attr", "userstory_custom_attributes_attr", + "task_custom_attributes_attr","issue_custom_attributes_attr", "roles_attr"]: + + assert hasattr(instance, attr), "instance must have a {} attribute".format(attr) + val = getattr(instance, attr) + if val is None: + continue + + for elem in val: + elem["name"] = _(elem["name"]) + + ret = super().to_value(instance) + + admin_fields = [ + "is_private_extra_info", "max_memberships", "issues_csv_uuid", + "tasks_csv_uuid", "userstories_csv_uuid", "transfer_token" + ] + + is_admin_user = False + if "request" in self.context: + user = self.context["request"].user + is_admin_user = permissions_services.is_project_admin(user, instance) + + if not is_admin_user: + for admin_field in admin_fields: + del(ret[admin_field]) + + return ret + + def get_members(self, obj): + assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute" + if obj.members_attr is None: + return [] + + ret = [] + for m in obj.members_attr: + m["full_name_display"] = m["full_name"] or m["username"] or m["email"] + del(m["email"]) + del(m["complete_user_name"]) + if not m["id"] is None: + ret.append(m) + + return ret def get_total_memberships(self, obj): - return services.get_total_project_memberships(obj) + if obj.members_attr is None: + return 0 + + return len(obj.members_attr) def get_is_out_of_owner_limits(self, obj): - return services.check_if_project_is_out_of_owner_limits(obj) - - -class ProjectDetailAdminSerializer(ProjectDetailSerializer): - is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info") - max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships") - - class Meta: - model = models.Project - read_only_fields = ("created_date", "modified_date", "slug", "blocked_code") - exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref") + assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" + assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" + return services.check_if_project_is_out_of_owner_limits(obj, + current_memberships = self.get_total_memberships(obj), + current_private_projects=obj.private_projects_same_owner_attr, + current_public_projects=obj.public_projects_same_owner_attr + ) def get_is_private_extra_info(self, obj): - return services.check_if_project_privacity_can_be_changed(obj) + assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" + assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" + return services.check_if_project_privacity_can_be_changed(obj, + current_memberships = self.get_total_memberships(obj), + current_private_projects=obj.private_projects_same_owner_attr, + current_public_projects=obj.public_projects_same_owner_attr + ) def get_max_memberships(self, obj): return services.get_max_memberships_for_project(obj) - ###################################################### ## Liked ###################################################### diff --git a/taiga/projects/services/projects.py b/taiga/projects/services/projects.py index f56a9941..2bd31d94 100644 --- a/taiga/projects/services/projects.py +++ b/taiga/projects/services/projects.py @@ -27,30 +27,45 @@ ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects' ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects' ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner' -def check_if_project_privacity_can_be_changed(project): +def check_if_project_privacity_can_be_changed(project, + current_memberships=None, + current_private_projects=None, + current_public_projects=None): """Return if the project privacity can be changed from private to public or viceversa. :param project: A project object. + :param current_memberships: Project total memberships, If None it will be calculated. + :param current_private_projects: total private projects owned by the project owner, If None it will be calculated. + :param current_public_projects: total public projects owned by the project owner, If None it will be calculated. :return: A dict like this {'can_be_updated': bool, 'reason': error message}. """ if project.owner is None: return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} - if project.is_private: + if current_memberships is None: current_memberships = project.memberships.count() + + if project.is_private: max_memberships = project.owner.max_memberships_public_projects error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS - current_projects = project.owner.owned_projects.filter(is_private=False).count() + if current_public_projects is None: + current_projects = project.owner.owned_projects.filter(is_private=False).count() + else: + current_projects = current_public_projects + max_projects = project.owner.max_public_projects error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS else: - current_memberships = project.memberships.count() max_memberships = project.owner.max_memberships_private_projects error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS - current_projects = project.owner.owned_projects.filter(is_private=True).count() + if current_private_projects is None: + current_projects = project.owner.owned_projects.filter(is_private=True).count() + else: + current_projects = current_private_projects + max_projects = project.owner.max_private_projects error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS @@ -139,25 +154,43 @@ def check_if_project_can_be_transfered(project, new_owner): return (True, None) -def check_if_project_is_out_of_owner_limits(project): +def check_if_project_is_out_of_owner_limits(project, + current_memberships=None, + current_private_projects=None, + current_public_projects=None): + """Return if the project fits on its owner limits. :param project: A project object. + :param current_memberships: Project total memberships, If None it will be calculated. + :param current_private_projects: total private projects owned by the project owner, If None it will be calculated. + :param current_public_projects: total public projects owned by the project owner, If None it will be calculated. :return: bool """ if project.owner is None: return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} - if project.is_private: + if current_memberships is None: current_memberships = project.memberships.count() + + if project.is_private: max_memberships = project.owner.max_memberships_private_projects - current_projects = project.owner.owned_projects.filter(is_private=True).count() + + if current_private_projects is None: + current_projects = project.owner.owned_projects.filter(is_private=True).count() + else: + current_projects = current_private_projects + max_projects = project.owner.max_private_projects else: - current_memberships = project.memberships.count() max_memberships = project.owner.max_memberships_public_projects - current_projects = project.owner.owned_projects.filter(is_private=False).count() + + if current_public_projects is None: + current_projects = project.owner.owned_projects.filter(is_private=False).count() + else: + current_projects = current_public_projects + max_projects = project.owner.max_public_projects if max_memberships is not None and current_memberships > max_memberships: diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py new file mode 100644 index 00000000..a05d8476 --- /dev/null +++ b/taiga/projects/utils.py @@ -0,0 +1,436 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +def attach_members(queryset, as_field="members_attr"): + """Attach a json members representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the members as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(t)) + FROM( + SELECT + users_user.id, + users_user.username, + users_user.full_name, + users_user.email, + concat(full_name, username) complete_user_name, + users_user.color, + users_user.photo, + users_user.is_active, + users_role.name role_name + + FROM projects_membership + LEFT JOIN users_user ON projects_membership.user_id = users_user.id + LEFT JOIN users_role ON users_role.id = projects_membership.role_id + WHERE projects_membership.project_id = {tbl}.id + ORDER BY complete_user_name) t""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_closed_milestones(queryset, as_field="closed_milestones_attr"): + """Attach a closed milestones counter to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the counter as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT COUNT(milestones_milestone.id) + FROM milestones_milestone + WHERE + milestones_milestone.project_id = {tbl}.id AND + milestones_milestone.closed = True + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_notify_policies(queryset, as_field="notify_policies_attr"): + """Attach a json notification policies representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the notification policies as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(notifications_notifypolicy)) + FROM notifications_notifypolicy + WHERE + notifications_notifypolicy.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_userstory_statuses(queryset, as_field="userstory_statuses_attr"): + """Attach a json userstory statuses representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the userstory statuses as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_userstorystatus)) + FROM projects_userstorystatus + WHERE + projects_userstorystatus.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_points(queryset, as_field="points_attr"): + """Attach a json points representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the points as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_points)) + FROM projects_points + WHERE + projects_points.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_task_statuses(queryset, as_field="task_statuses_attr"): + """Attach a json task statuses representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the task statuses as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_taskstatus)) + FROM projects_taskstatus + WHERE + projects_taskstatus.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_issue_statuses(queryset, as_field="issue_statuses_attr"): + """Attach a json issue statuses representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the statuses as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_issuestatus)) + FROM projects_issuestatus + WHERE + projects_issuestatus.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_issue_types(queryset, as_field="issue_types_attr"): + """Attach a json issue types representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the types as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_issuetype)) + FROM projects_issuetype + WHERE + projects_issuetype.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_priorities(queryset, as_field="priorities_attr"): + """Attach a json priorities representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the priorities as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_priority)) + FROM projects_priority + WHERE + projects_priority.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_severities(queryset, as_field="severities_attr"): + """Attach a json severities representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the severities as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_severity)) + FROM projects_severity + WHERE + projects_severity.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_userstory_custom_attributes(queryset, as_field="userstory_custom_attributes_attr"): + """Attach a json userstory custom attributes representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the userstory custom attributes as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(custom_attributes_userstorycustomattribute)) + FROM custom_attributes_userstorycustomattribute + WHERE + custom_attributes_userstorycustomattribute.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_task_custom_attributes(queryset, as_field="task_custom_attributes_attr"): + """Attach a json task custom attributes representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the task custom attributes as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(custom_attributes_taskcustomattribute)) + FROM custom_attributes_taskcustomattribute + WHERE + custom_attributes_taskcustomattribute.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_issue_custom_attributes(queryset, as_field="issue_custom_attributes_attr"): + """Attach a json issue custom attributes representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the issue custom attributes as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(custom_attributes_issuecustomattribute)) + FROM custom_attributes_issuecustomattribute + WHERE + custom_attributes_issuecustomattribute.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_roles(queryset, as_field="roles_attr"): + """Attach a json roles representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the roles as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(users_role)) + FROM users_role + WHERE + users_role.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_is_fan(queryset, user, as_field="is_fan_attr"): + """Attach a is fan boolean to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the boolean as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + if user is None or user.is_anonymous(): + sql = """SELECT false""" + else: + sql = """SELECT COUNT(likes_like.id) > 0 + FROM likes_like + INNER JOIN django_content_type + ON likes_like.content_type_id = django_content_type.id + WHERE + django_content_type.model = 'project' AND + django_content_type.app_label = 'projects' AND + likes_like.user_id = {user_id} AND + likes_like.object_id = {tbl}.id""" + + sql = sql.format(tbl=model._meta.db_table, user_id=user.id) + + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_my_role_permissions(queryset, user, as_field="my_role_permissions_attr"): + """Attach a permission array to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the permissions as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + if user is None or user.is_anonymous(): + sql = """SELECT '{}'""" + else: + sql = """SELECT users_role.permissions + FROM projects_membership + LEFT JOIN users_user ON projects_membership.user_id = users_user.id + LEFT JOIN users_role ON users_role.id = projects_membership.role_id + WHERE + projects_membership.project_id = {tbl}.id AND + users_user.id = {user_id}""" + + sql = sql.format(tbl=model._meta.db_table, user_id=user.id) + + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_private_projects_same_owner(queryset, user, as_field="private_projects_same_owner_attr"): + """Attach a private projects counter to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the counter as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + if user is None or user.is_anonymous(): + sql = """SELECT '0'""" + else: + sql = """SELECT COUNT(id) + FROM projects_project p_aux + WHERE + p_aux.is_private = True AND + p_aux.owner_id = {tbl}.owner_id""" + + sql = sql.format(tbl=model._meta.db_table, user_id=user.id) + + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_public_projects_same_owner(queryset, user, as_field="public_projects_same_owner_attr"): + """Attach a public projects counter to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the counter as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + if user is None or user.is_anonymous(): + sql = """SELECT '0'""" + else: + sql = """SELECT COUNT(id) + FROM projects_project p_aux + WHERE + p_aux.is_private = False AND + p_aux.owner_id = {tbl}.owner_id""" + + sql = sql.format(tbl=model._meta.db_table, user_id=user.id) + + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_extra_info(queryset, user=None): + queryset = attach_members(queryset) + queryset = attach_closed_milestones(queryset) + queryset = attach_notify_policies(queryset) + queryset = attach_userstory_statuses(queryset) + queryset = attach_points(queryset) + queryset = attach_task_statuses(queryset) + queryset = attach_issue_statuses(queryset) + queryset = attach_issue_types(queryset) + queryset = attach_priorities(queryset) + queryset = attach_severities(queryset) + queryset = attach_userstory_custom_attributes(queryset) + queryset = attach_task_custom_attributes(queryset) + queryset = attach_issue_custom_attributes(queryset) + queryset = attach_roles(queryset) + queryset = attach_is_fan(queryset, user) + queryset = attach_my_role_permissions(queryset, user) + queryset = attach_private_projects_same_owner(queryset, user) + queryset = attach_public_projects_same_owner(queryset, user) + + return queryset diff --git a/taiga/users/models.py b/taiga/users/models.py index 264d1539..b9c60e4b 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -198,7 +198,7 @@ class User(AbstractBaseUser, PermissionsMixin): def _fill_cached_memberships(self): self._cached_memberships = {} - qs = self.memberships.prefetch_related("user", "project", "role") + qs = self.memberships.select_related("user", "project", "role") for membership in qs.all(): self._cached_memberships[membership.project.id] = membership diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 77e79542..949893b6 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -22,7 +22,7 @@ from django.apps import apps from taiga.base.utils import json from taiga.projects import choices as project_choices -from taiga.projects.serializers import ProjectDetailSerializer +from taiga.projects.serializers import ProjectSerializer from taiga.permissions.choices import MEMBERS_PERMISSIONS from tests import factories as f @@ -153,12 +153,12 @@ def test_project_update(client, data): data.project_owner ] - project_data = ProjectDetailSerializer(data.private_project2).data + project_data = ProjectSerializer(data.private_project2).data project_data["is_private"] = False results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users) assert results == [401, 403, 403, 200] - project_data = ProjectDetailSerializer(data.blocked_project).data + project_data = ProjectSerializer(data.blocked_project).data project_data["is_private"] = False results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users) assert results == [401, 403, 403, 451] diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 5cadc245..cc3b3cb1 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -625,7 +625,7 @@ def test_projects_user_order(client): #Testing user order url = reverse("projects-list") - url = "%s?member=%s&order_by=memberships__user_order" % (url, user.id) + url = "%s?member=%s&order_by=user_order" % (url, user.id) response = client.json.get(url) response_content = response.data assert response.status_code == 200 From 4864b9f95759398d6bc72b61327dd37fa99e8c96 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 29 Jun 2016 09:45:00 +0200 Subject: [PATCH 084/261] Splitting validators and serializers --- taiga/base/api/generics.py | 38 +- taiga/base/api/mixins.py | 39 +- taiga/base/api/serializers.py | 2 + taiga/base/api/settings.py | 2 + .../serializers.py => base/api/validators.py} | 13 +- taiga/base/fields.py | 84 ++- taiga/base/neighbors.py | 18 +- taiga/export_import/api.py | 5 +- taiga/projects/api.py | 56 +- taiga/projects/attachments/serializers.py | 9 +- taiga/projects/filters.py | 24 +- taiga/projects/history/api.py | 3 +- taiga/projects/history/models.py | 19 +- taiga/projects/history/serializers.py | 37 +- taiga/projects/history/services.py | 76 ++- taiga/projects/issues/api.py | 21 +- taiga/projects/issues/serializers.py | 106 +-- taiga/projects/issues/utils.py | 57 ++ taiga/projects/issues/validators.py | 43 ++ taiga/projects/milestones/api.py | 42 +- taiga/projects/milestones/serializers.py | 67 +- taiga/projects/milestones/utils.py | 44 +- taiga/projects/milestones/validators.py | 13 +- taiga/projects/mixins/serializers.py | 52 +- taiga/projects/notifications/mixins.py | 50 +- taiga/projects/notifications/utils.py | 21 +- taiga/projects/serializers.py | 610 +++++++----------- taiga/projects/tasks/api.py | 38 +- taiga/projects/tasks/serializers.py | 164 ++--- taiga/projects/tasks/utils.py | 39 ++ taiga/projects/tasks/validators.py | 38 +- taiga/projects/userstories/api.py | 61 +- taiga/projects/userstories/serializers.py | 298 +++------ taiga/projects/userstories/utils.py | 40 +- taiga/projects/userstories/validators.py | 81 +++ taiga/projects/utils.py | 4 +- taiga/projects/validators.py | 198 ++++++ taiga/projects/votes/mixins/serializers.py | 18 +- taiga/projects/votes/mixins/viewsets.py | 8 - taiga/projects/votes/utils.py | 26 +- taiga/projects/wiki/api.py | 9 +- taiga/projects/wiki/serializers.py | 33 +- taiga/projects/wiki/utils.py | 29 + taiga/projects/wiki/validators.py | 34 + taiga/searches/serializers.py | 63 +- taiga/searches/services.py | 11 +- taiga/timeline/api.py | 4 +- .../migrations/0005_auto_20160706_0723.py | 21 + taiga/timeline/models.py | 6 +- taiga/timeline/serializers.py | 18 +- taiga/timeline/service.py | 55 +- taiga/users/serializers.py | 36 +- taiga/webhooks/api.py | 2 + taiga/webhooks/serializers.py | 324 +++++----- taiga/webhooks/tasks.py | 1 - taiga/webhooks/validators.py | 26 + .../test_issues_resources.py | 54 +- .../test_milestones_resources.py | 62 +- .../test_projects_resource.py | 9 + .../test_tasks_resources.py | 143 ++-- .../test_userstories_resources.py | 122 ++-- .../test_webhooks_resources.py | 3 + tests/integration/test_milestones.py | 2 +- tests/integration/test_notifications.py | 43 +- tests/integration/test_userstories.py | 113 ++-- tests/integration/test_webhooks_issues.py | 5 +- tests/unit/test_serializer_mixins.py | 25 +- 67 files changed, 2077 insertions(+), 1740 deletions(-) rename taiga/{projects/likes/mixins/serializers.py => base/api/validators.py} (72%) create mode 100644 taiga/projects/issues/utils.py create mode 100644 taiga/projects/issues/validators.py create mode 100644 taiga/projects/tasks/utils.py create mode 100644 taiga/projects/wiki/utils.py create mode 100644 taiga/projects/wiki/validators.py create mode 100644 taiga/timeline/migrations/0005_auto_20160706_0723.py create mode 100644 taiga/webhooks/validators.py diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index 158d712d..31823945 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -62,6 +62,7 @@ class GenericAPIView(pagination.PaginationMixin, # or override `get_queryset()`/`get_serializer_class()`. queryset = None serializer_class = None + validator_class = None # This shortcut may be used instead of setting either or both # of the `queryset`/`serializer_class` attributes, although using @@ -79,6 +80,7 @@ class GenericAPIView(pagination.PaginationMixin, # The following attributes may be subject to change, # and should be considered private API. model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + model_validator_class = api_settings.DEFAULT_MODEL_VALIDATOR_CLASS ###################################### # These are pending deprecation... @@ -88,7 +90,7 @@ class GenericAPIView(pagination.PaginationMixin, slug_field = 'slug' allow_empty = True - def get_serializer_context(self): + def get_extra_context(self): """ Extra context provided to the serializer class. """ @@ -101,14 +103,24 @@ class GenericAPIView(pagination.PaginationMixin, def get_serializer(self, instance=None, data=None, files=None, many=False, partial=False): """ - Return the serializer instance that should be used for validating and - deserializing input, and for serializing output. + Return the serializer instance that should be used for deserializing + input, and for serializing output. """ serializer_class = self.get_serializer_class() - context = self.get_serializer_context() + context = self.get_extra_context() return serializer_class(instance, data=data, files=files, many=many, partial=partial, context=context) + def get_validator(self, instance=None, data=None, + files=None, many=False, partial=False): + """ + Return the validator instance that should be used for validating the + input, and for serializing output. + """ + validator_class = self.get_validator_class() + context = self.get_extra_context() + return validator_class(instance, data=data, files=files, + many=many, partial=partial, context=context) def filter_queryset(self, queryset, filter_backends=None): """ @@ -119,7 +131,7 @@ class GenericAPIView(pagination.PaginationMixin, method if you want to apply the configured filtering backend to the default queryset. """ - #NOTE TAIGA: Added filter_backends to overwrite the default behavior. + # NOTE TAIGA: Added filter_backends to overwrite the default behavior. backends = filter_backends or self.get_filter_backends() for backend in backends: @@ -160,6 +172,22 @@ class GenericAPIView(pagination.PaginationMixin, model = self.model return DefaultSerializer + def get_validator_class(self): + validator_class = self.validator_class + serializer_class = self.get_serializer_class() + + # Situations where the validator is the rest framework serializer + if validator_class is None and serializer_class is not None: + return serializer_class + + if validator_class is not None: + return validator_class + + class DefaultValidator(self.model_validator_class): + class Meta: + model = self.model + return DefaultValidator + def get_queryset(self): """ Get the list of items for this view. diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index 89af6984..861d77ec 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -57,6 +57,7 @@ from .utils import get_object_or_404 from .. import exceptions as exc from ..decorators import model_pk_lock + def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): """ Given a model instance, and an optional pk and slug field, @@ -89,19 +90,21 @@ class CreateModelMixin: Create a model instance. """ def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.DATA, files=request.FILES) + validator = self.get_validator(data=request.DATA, files=request.FILES) - if serializer.is_valid(): - self.check_permissions(request, 'create', serializer.object) + if validator.is_valid(): + self.check_permissions(request, 'create', validator.object) - self.pre_save(serializer.object) - self.pre_conditions_on_save(serializer.object) - self.object = serializer.save(force_insert=True) + self.pre_save(validator.object) + self.pre_conditions_on_save(validator.object) + self.object = validator.save(force_insert=True) self.post_save(self.object, created=True) + instance = self.get_queryset().get(id=self.object.id) + serializer = self.get_serializer(instance) headers = self.get_success_headers(serializer.data) return response.Created(serializer.data, headers=headers) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) def get_success_headers(self, data): try: @@ -171,28 +174,32 @@ class UpdateModelMixin: if self.object is None: raise Http404 - serializer = self.get_serializer(self.object, data=request.DATA, - files=request.FILES, partial=partial) + validator = self.get_validator(self.object, data=request.DATA, + files=request.FILES, partial=partial) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + if not validator.is_valid(): + return response.BadRequest(validator.errors) # Hooks try: - self.pre_save(serializer.object) - self.pre_conditions_on_save(serializer.object) + self.pre_save(validator.object) + self.pre_conditions_on_save(validator.object) except ValidationError as err: # full_clean on model instance may be called in pre_save, # so we have to handle eventual errors. return response.BadRequest(err.message_dict) if self.object is None: - self.object = serializer.save(force_insert=True) + self.object = validator.save(force_insert=True) self.post_save(self.object, created=True) + instance = self.get_queryset().get(id=self.object.id) + serializer = self.get_serializer(instance) return response.Created(serializer.data) - self.object = serializer.save(force_update=True) + self.object = validator.save(force_update=True) self.post_save(self.object, created=False) + instance = self.get_queryset().get(id=self.object.id) + serializer = self.get_serializer(instance) return response.Ok(serializer.data) def partial_update(self, request, *args, **kwargs): @@ -251,7 +258,7 @@ class BlockeableModelMixin: raise NotImplementedError("is_blocked must be overridden") def pre_conditions_blocked(self, obj): - #Raises permission exception + # Raises permission exception if obj is not None and self.is_blocked(obj): raise exc.Blocked(_("Blocked element")) diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 601c1753..82565b26 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -1229,5 +1229,7 @@ class LightSerializer(serpy.Serializer): kwargs.pop("partial", None) kwargs.pop("files", None) context = kwargs.pop("context", {}) + view = kwargs.pop("view", {}) super().__init__(*args, **kwargs) self.context = context + self.view = view diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py index 1a3d01ba..75d204c9 100644 --- a/taiga/base/api/settings.py +++ b/taiga/base/api/settings.py @@ -98,6 +98,8 @@ DEFAULTS = { # Genric view behavior "DEFAULT_MODEL_SERIALIZER_CLASS": "taiga.base.api.serializers.ModelSerializer", + "DEFAULT_MODEL_VALIDATOR_CLASS": + "taiga.base.api.validators.ModelValidator", "DEFAULT_FILTER_BACKENDS": (), # Throttling diff --git a/taiga/projects/likes/mixins/serializers.py b/taiga/base/api/validators.py similarity index 72% rename from taiga/projects/likes/mixins/serializers.py rename to taiga/base/api/validators.py index 84d63b4e..3a8d6922 100644 --- a/taiga/projects/likes/mixins/serializers.py +++ b/taiga/base/api/validators.py @@ -16,15 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api import serializers +from . import serializers -class FanResourceSerializerMixin(serializers.ModelSerializer): - is_fan = serializers.SerializerMethodField("get_is_fan") +class Validator(serializers.Serializer): + pass - def get_is_fan(self, obj): - if "request" in self.context: - user = self.context["request"].user - return user.is_authenticated() and user.is_fan(obj) - return False +class ModelValidator(serializers.ModelSerializer): + pass diff --git a/taiga/base/fields.py b/taiga/base/fields.py index 5e5c4b5a..f0cf4ee2 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -20,11 +20,13 @@ from django.forms import widgets from django.utils.translation import ugettext as _ from taiga.base.api import serializers +import serpy #################################################################### -## Serializer fields +# Serializer fields #################################################################### + class JsonField(serializers.WritableField): """ Json objects serializer. @@ -38,40 +40,6 @@ class JsonField(serializers.WritableField): return data -class I18NJsonField(JsonField): - """ - Json objects serializer. - """ - widget = widgets.Textarea - - def __init__(self, i18n_fields=(), *args, **kwargs): - super(I18NJsonField, self).__init__(*args, **kwargs) - self.i18n_fields = i18n_fields - - def translate_values(self, d): - i18n_d = {} - if d is None: - return d - - for key, value in d.items(): - if isinstance(value, dict): - i18n_d[key] = self.translate_values(value) - - if key in self.i18n_fields: - if isinstance(value, list): - i18n_d[key] = [e is not None and _(str(e)) or e for e in value] - if isinstance(value, str): - i18n_d[key] = value is not None and _(value) or value - else: - i18n_d[key] = value - - return i18n_d - - def to_native(self, obj): - i18n_obj = self.translate_values(obj) - return i18n_obj - - class PgArrayField(serializers.WritableField): """ PgArray objects serializer. @@ -104,3 +72,49 @@ class WatchersField(serializers.WritableField): def from_native(self, data): return data + + +class Field(serpy.Field): + pass + + +class MethodField(serpy.MethodField): + pass + + +class I18NField(serpy.Field): + def to_value(self, value): + ret = super(I18NField, self).to_value(value) + return _(ret) + + +class I18NJsonField(serpy.Field): + """ + Json objects serializer. + """ + def __init__(self, i18n_fields=(), *args, **kwargs): + super(I18NJsonField, self).__init__(*args, **kwargs) + self.i18n_fields = i18n_fields + + def translate_values(self, d): + i18n_d = {} + if d is None: + return d + + for key, value in d.items(): + if isinstance(value, dict): + i18n_d[key] = self.translate_values(value) + + if key in self.i18n_fields: + if isinstance(value, list): + i18n_d[key] = [e is not None and _(str(e)) or e for e in value] + if isinstance(value, str): + i18n_d[key] = value is not None and _(value) or value + else: + i18n_d[key] = value + + return i18n_d + + def to_native(self, obj): + i18n_obj = self.translate_values(obj) + return i18n_obj diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py index a57d2eeb..c8733ade 100644 --- a/taiga/base/neighbors.py +++ b/taiga/base/neighbors.py @@ -23,6 +23,7 @@ from django.db import connection from django.core.exceptions import ObjectDoesNotExist from django.db.models.sql.datastructures import EmptyResultSet from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField Neighbor = namedtuple("Neighbor", "left right") @@ -71,7 +72,6 @@ def get_neighbors(obj, results_set=None): if row is None: return Neighbor(None, None) - obj_position = row[1] - 1 left_object_id = row[2] right_object_id = row[3] @@ -88,13 +88,19 @@ def get_neighbors(obj, results_set=None): return Neighbor(left, right) -class NeighborsSerializerMixin: - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors") +class NeighborSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + + +class NeighborsSerializerMixin(serializers.LightSerializer): + neighbors = MethodField() def serialize_neighbor(self, neighbor): - raise NotImplementedError + if neighbor: + return NeighborSerializer(neighbor).data + return None def get_neighbors(self, obj): view, request = self.context.get("view", None), self.context.get("request", None) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index d8453ad5..eaff499d 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -34,6 +34,7 @@ from taiga.base import exceptions as exc from taiga.base import response from taiga.base.api.mixins import CreateModelMixin from taiga.base.api.viewsets import GenericViewSet +from taiga.projects import utils as project_utils from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task @@ -366,5 +367,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi return response.BadRequest({"error": e.message, "details": e.errors}) else: # On Success - response_data = ProjectSerializer(project).data + project_from_qs = project_utils.attach_extra_info(Project.objects.all()).get(id=project.id) + response_data = ProjectSerializer(project_from_qs).data + return response.Created(response_data) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index c6fbbe0d..c441e419 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -23,9 +23,6 @@ from dateutil.relativedelta import relativedelta from django.apps import apps from django.conf import settings from django.core.exceptions import ValidationError -from django.db.models import signals, Prefetch -from django.db.models import Value as V -from django.db.models.functions import Coalesce from django.http import Http404 from django.utils.translation import ugettext as _ from django.utils import timezone @@ -45,8 +42,7 @@ from taiga.permissions import services as permissions_services from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.issues.models import Issue from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin -from taiga.projects.notifications.models import NotifyPolicy -from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin +from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.mixins.ordering import BulkUpdateOrderMixin @@ -54,21 +50,24 @@ from taiga.projects.tasks.models import Task from taiga.projects.tagging.api import TagsColorsResourceMixin from taiga.projects.userstories.models import UserStory, RolePoints -from taiga.users import services as users_services from . import filters as project_filters from . import models from . import permissions from . import serializers +from . import validators from . import services from . import utils as project_utils ###################################################### -## Project +# Project ###################################################### -class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin, + +class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, + BlockeableSaveMixin, BlockeableDeleteMixin, TagsColorsResourceMixin, ModelCrudViewSet): + validator_class = validators.ProjectValidator queryset = models.Project.objects.all() permission_classes = (permissions.ProjectPermission, ) filter_backends = (project_filters.UserOrderFilterBackend, @@ -132,12 +131,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix def get_serializer_class(self): if self.action == "list": - return serializers.LightProjectSerializer + return serializers.ProjectSerializer - if self.action in ["retrieve", "by_slug"]: - return serializers.LightProjectDetailSerializer - - return serializers.ProjectSerializer + return serializers.ProjectDetailSerializer @detail_route(methods=["POST"]) def change_logo(self, request, *args, **kwargs): @@ -200,11 +196,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix if self.request.user.is_anonymous(): return response.Unauthorized() - serializer = serializers.UpdateProjectOrderBulkSerializer(data=request.DATA, many=True) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.UpdateProjectOrderBulkValidator(data=request.DATA, many=True) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data services.update_projects_order_in_bulk(data, "user_order", request.user) return response.NoContent(data=None) @@ -346,7 +342,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix return response.BadRequest(_("The user must be already a project member")) reason = request.DATA.get('reason', None) - transfer_token = services.start_project_transfer(project, user, reason) + services.start_project_transfer(project, user, reason) return response.Ok() @detail_route(methods=["POST"]) @@ -455,6 +451,7 @@ class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, model = models.Points serializer_class = serializers.PointsSerializer + validator_class = validators.PointsValidator permission_classes = (permissions.PointsPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) @@ -471,6 +468,7 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, model = models.UserStoryStatus serializer_class = serializers.UserStoryStatusSerializer + validator_class = validators.UserStoryStatusValidator permission_classes = (permissions.UserStoryStatusPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) @@ -487,6 +485,7 @@ class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, model = models.TaskStatus serializer_class = serializers.TaskStatusSerializer + validator_class = validators.TaskStatusValidator permission_classes = (permissions.TaskStatusPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -503,6 +502,7 @@ class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, model = models.Severity serializer_class = serializers.SeveritySerializer + validator_class = validators.SeverityValidator permission_classes = (permissions.SeverityPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -518,6 +518,7 @@ class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, ModelCrudViewSet, BulkUpdateOrderMixin): model = models.Priority serializer_class = serializers.PrioritySerializer + validator_class = validators.PriorityValidator permission_classes = (permissions.PriorityPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -533,6 +534,7 @@ class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, ModelCrudViewSet, BulkUpdateOrderMixin): model = models.IssueType serializer_class = serializers.IssueTypeSerializer + validator_class = validators.IssueTypeValidator permission_classes = (permissions.IssueTypePermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -548,6 +550,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, ModelCrudViewSet, BulkUpdateOrderMixin): model = models.IssueStatus serializer_class = serializers.IssueStatusSerializer + validator_class = validators.IssueStatusValidator permission_classes = (permissions.IssueStatusPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -566,6 +569,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, class ProjectTemplateViewSet(ModelCrudViewSet): model = models.ProjectTemplate serializer_class = serializers.ProjectTemplateSerializer + validator_class = validators.ProjectTemplateValidator permission_classes = (permissions.ProjectTemplatePermission,) def get_queryset(self): @@ -579,7 +583,9 @@ class ProjectTemplateViewSet(ModelCrudViewSet): class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Membership admin_serializer_class = serializers.MembershipAdminSerializer + admin_validator_class = validators.MembershipAdminValidator serializer_class = serializers.MembershipSerializer + validator_class = validators.MembershipValidator permission_classes = (permissions.MembershipPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project", "role") @@ -604,6 +610,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): else: return self.serializer_class + def get_validator_class(self): + if self.action == "create": + return self.admin_validator_class + + return self.validator_class + def _check_if_project_can_have_more_memberships(self, project, total_new_memberships): (can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships( project, @@ -618,11 +630,11 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): - serializer = serializers.MembersBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.MembersBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data project = models.Project.objects.get(id=data["project_id"]) invitation_extra_text = data.get("invitation_extra_text", None) self.check_permissions(request, 'bulk_create', project) diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 6c5ee05b..45e6be45 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -19,14 +19,12 @@ from django.conf import settings from taiga.base.api import serializers +from taiga.base.fields import MethodField from taiga.base.utils.thumbnails import get_thumbnail_url from . import services from . import models -import json -import serpy - class AttachmentSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField("get_url") @@ -43,12 +41,11 @@ class AttachmentSerializer(serializers.ModelSerializer): def get_url(self, obj): return obj.attached_file.url - def get_thumbnail_card_url(self, obj): return services.get_card_image_thumbnail_url(obj) -class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer): +class BasicAttachmentsInfoSerializerMixin(serializers.LightSerializer): """ Assumptions: - The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information @@ -56,7 +53,7 @@ class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer): - The method attach_basic_attachments has been used to include the necessary json data about the attachments in the "attachments_attr" column """ - attachments = serpy.MethodField() + attachments = MethodField() def get_attachments(self, obj): include_attachments = getattr(obj, "include_attachments", False) diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py index cbb692b8..fe720f97 100644 --- a/taiga/projects/filters.py +++ b/taiga/projects/filters.py @@ -97,12 +97,12 @@ class QFilterBackend(FilterBackend): tsquery = "to_tsquery('english_nostop', %s)" tsquery_params = [to_tsquery(q)] tsvector = """ - setweight(to_tsvector('english_nostop', - coalesce(projects_project.name, '')), 'A') || - setweight(to_tsvector('english_nostop', - coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') || - setweight(to_tsvector('english_nostop', - coalesce(projects_project.description, '')), 'C') + setweight(to_tsvector('english_nostop', + coalesce(projects_project.name, '')), 'A') || + setweight(to_tsvector('english_nostop', + coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') || + setweight(to_tsvector('english_nostop', + coalesce(projects_project.description, '')), 'C') """ select = { @@ -111,7 +111,7 @@ class QFilterBackend(FilterBackend): } select_params = tsquery_params where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery, - tsvector=tsvector),] + tsvector=tsvector), ] params = tsquery_params order_by = ["-rank", ] @@ -142,11 +142,11 @@ class UserOrderFilterBackend(FilterBackend): model = queryset.model sql = """SELECT projects_membership.user_order - FROM projects_membership - WHERE - projects_membership.project_id = {tbl}.id AND - projects_membership.user_id = {user_id} - """ + FROM projects_membership + WHERE + projects_membership.project_id = {tbl}.id AND + projects_membership.user_id = {user_id} + """ sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id) queryset = queryset.extra(select={"user_order": sql}) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index a4c8199e..2119239a 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -23,7 +23,6 @@ from django.utils import timezone from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ReadOnlyListViewSet -from taiga.base.api.utils import get_object_or_404 from taiga.mdrender.service import render as mdrender from . import permissions @@ -38,7 +37,7 @@ class HistoryViewSet(ReadOnlyListViewSet): def get_content_type(self): app_name, model = self.content_type.split(".", 1) - return get_object_or_404(ContentType, app_label=app_name, model=model) + return ContentType.objects.get_by_natural_key(app_name, model) def get_queryset(self): ct = self.get_content_type() diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 558c5c25..d5c023a9 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -33,7 +33,8 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts # This keys has been removed from freeze_impl so we can have objects where the # previous diff has value for the attribute and we want to prevent their propagation -IGNORE_DIFF_FIELDS = [ "watchers", "description_diff", "content_diff", "blocked_note_diff"] +IGNORE_DIFF_FIELDS = ["watchers", "description_diff", "content_diff", "blocked_note_diff"] + def _generate_uuid(): return str(uuid.uuid1()) @@ -92,15 +93,15 @@ class HistoryEntry(models.Model): @cached_property def is_change(self): - return self.type == HistoryType.change + return self.type == HistoryType.change @cached_property def is_create(self): - return self.type == HistoryType.create + return self.type == HistoryType.create @cached_property def is_delete(self): - return self.type == HistoryType.delete + return self.type == HistoryType.delete @property def owner(self): @@ -185,7 +186,7 @@ class HistoryEntry(models.Model): role_name = resolve_value("roles", role_id) oldpoint_id = pointsold.get(role_id, None) points[role_name] = [resolve_value("points", oldpoint_id), - resolve_value("points", point_id)] + resolve_value("points", point_id)] # Process that removes points entries with # duplicate value. @@ -204,8 +205,8 @@ class HistoryEntry(models.Model): "deleted": [], } - oldattachs = {x["id"]:x for x in self.diff["attachments"][0]} - newattachs = {x["id"]:x for x in self.diff["attachments"][1]} + oldattachs = {x["id"]: x for x in self.diff["attachments"][0]} + newattachs = {x["id"]: x for x in self.diff["attachments"][1]} for aid in set(tuple(oldattachs.keys()) + tuple(newattachs.keys())): if aid in oldattachs and aid in newattachs: @@ -235,8 +236,8 @@ class HistoryEntry(models.Model): "deleted": [], } - oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0] or []} - newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1] or []} + oldcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][0] or []} + newcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][1] or []} for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())): if aid in oldcustattrs and aid in newcustattrs: diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index f1a10481..8407810f 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -17,28 +17,31 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.base.fields import JsonField, I18NJsonField +from taiga.base.fields import I18NJsonField, Field, MethodField from taiga.users.services import get_photo_or_gravatar_url -from . import models + +HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type") -HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type") - - -class HistoryEntrySerializer(serializers.ModelSerializer): - diff = JsonField() - snapshot = JsonField() - values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) - values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) - user = serializers.SerializerMethodField("get_user") - delete_comment_user = JsonField() - comment_versions = JsonField() - - class Meta: - model = models.HistoryEntry - exclude = ("comment_versions",) +class HistoryEntrySerializer(serializers.LightSerializer): + id = Field() + user = MethodField() + created_at = Field() + type = Field() + key = Field() + diff = Field() + snapshot = Field() + values = Field() + values_diff = I18NJsonField() + comment = I18NJsonField() + comment_html = Field() + delete_comment_date = Field() + delete_comment_user = Field() + edit_comment_date = Field() + is_hidden = Field() + is_snapshot = Field() def get_user(self, entry): user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 71b5bcf8..764cca39 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -34,12 +34,9 @@ from collections import namedtuple from copy import deepcopy from functools import partial from functools import wraps -from functools import lru_cache from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -from django.core.paginator import Paginator, InvalidPage from django.apps import apps from django.db import transaction as tx from django_pglocks import advisory_lock @@ -50,6 +47,21 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts from .models import HistoryType +# Freeze implementatitions +from .freeze_impl import project_freezer +from .freeze_impl import milestone_freezer +from .freeze_impl import userstory_freezer +from .freeze_impl import issue_freezer +from .freeze_impl import task_freezer +from .freeze_impl import wikipage_freezer + + +from .freeze_impl import project_values +from .freeze_impl import milestone_values +from .freeze_impl import userstory_values +from .freeze_impl import issue_values +from .freeze_impl import task_values +from .freeze_impl import wikipage_values # Type that represents a freezed object FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"]) @@ -71,7 +83,7 @@ _not_important_fields = { log = logging.getLogger("taiga.history") -def make_key_from_model_object(obj:object) -> str: +def make_key_from_model_object(obj: object) -> str: """ Create unique key from model instance. """ @@ -79,7 +91,7 @@ def make_key_from_model_object(obj:object) -> str: return "{0}:{1}".format(tn, obj.pk) -def get_model_from_key(key:str) -> object: +def get_model_from_key(key: str) -> object: """ Get model from key """ @@ -87,7 +99,7 @@ def get_model_from_key(key:str) -> object: return apps.get_model(class_name) -def get_pk_from_key(key:str) -> object: +def get_pk_from_key(key: str) -> object: """ Get pk from key """ @@ -95,7 +107,7 @@ def get_pk_from_key(key:str) -> object: return pk -def get_instance_from_key(key:str) -> object: +def get_instance_from_key(key: str) -> object: """ Get instance from key """ @@ -109,7 +121,7 @@ def get_instance_from_key(key:str) -> object: return None -def register_values_implementation(typename:str, fn=None): +def register_values_implementation(typename: str, fn=None): """ Register values implementation for specified typename. This function can be used as decorator. @@ -128,7 +140,7 @@ def register_values_implementation(typename:str, fn=None): return _wrapper -def register_freeze_implementation(typename:str, fn=None): +def register_freeze_implementation(typename: str, fn=None): """ Register freeze implementation for specified typename. This function can be used as decorator. @@ -149,7 +161,7 @@ def register_freeze_implementation(typename:str, fn=None): # Low level api -def freeze_model_instance(obj:object) -> FrozenObj: +def freeze_model_instance(obj: object) -> FrozenObj: """ Creates a new frozen object from model instance. @@ -179,7 +191,7 @@ def freeze_model_instance(obj:object) -> FrozenObj: return FrozenObj(key, snapshot) -def is_hidden_snapshot(obj:FrozenDiff) -> bool: +def is_hidden_snapshot(obj: FrozenDiff) -> bool: """ Check if frozen object is considered hidden or not. @@ -199,7 +211,7 @@ def is_hidden_snapshot(obj:FrozenDiff) -> bool: return False -def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff: +def make_diff(oldobj: FrozenObj, newobj: FrozenObj) -> FrozenDiff: """ Compute a diff between two frozen objects. """ @@ -217,7 +229,7 @@ def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff: return FrozenDiff(newobj.key, diff, newobj.snapshot) -def make_diff_values(typename:str, fdiff:FrozenDiff) -> dict: +def make_diff_values(typename: str, fdiff: FrozenDiff) -> dict: """ Given a typename and diff, build a values dict for it. If no implementation found for typename, warnig is raised in @@ -242,7 +254,7 @@ def _rebuild_snapshot_from_diffs(keysnapshot, partials): return result -def get_last_snapshot_for_key(key:str) -> FrozenObj: +def get_last_snapshot_for_key(key: str) -> FrozenObj: entry_model = apps.get_model("history", "HistoryEntry") # Search last snapshot @@ -271,17 +283,16 @@ def get_last_snapshot_for_key(key:str) -> FrozenObj: # Public api -def get_modified_fields(obj:object, last_modifications): +def get_modified_fields(obj: object, last_modifications): """ Get the modified fields for an object through his last modifications """ key = make_key_from_model_object(obj) entry_model = apps.get_model("history", "HistoryEntry") history_entries = (entry_model.objects - .filter(key=key) - .order_by("-created_at") - .values_list("diff", flat=True) - [0:last_modifications]) + .filter(key=key) + .order_by("-created_at") + .values_list("diff", flat=True)[0:last_modifications]) modified_fields = [] for history_entry in history_entries: @@ -291,7 +302,7 @@ def get_modified_fields(obj:object, last_modifications): @tx.atomic -def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): +def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False): """ Given any model instance with registred content type, create new history entry of "change" type. @@ -301,7 +312,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): """ key = make_key_from_model_object(obj) - with advisory_lock(key) as acquired_key_lock: + with advisory_lock(key): typename = get_typename_for_model_class(obj.__class__) new_fobj = freeze_model_instance(obj) @@ -327,8 +338,8 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): # If diff and comment are empty, do # not create empty history entry if (not fdiff.diff and not comment - and old_fobj is not None - and entry_type != HistoryType.delete): + and old_fobj is not None + and entry_type != HistoryType.delete): return None @@ -358,7 +369,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): # High level query api -def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,), +def get_history_queryset_by_model_instance(obj: object, types=(HistoryType.change,), include_hidden=False): """ Get one page of history for specified object. @@ -377,20 +388,12 @@ def prefetch_owners_in_history_queryset(qs): user_ids = [u["pk"] for u in qs.values_list("user", flat=True)] users = get_user_model().objects.filter(id__in=user_ids) users_by_id = {u.id: u for u in users} - for history_entry in qs: + for history_entry in qs: history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None)) return qs -# Freeze implementatitions -from .freeze_impl import project_freezer -from .freeze_impl import milestone_freezer -from .freeze_impl import userstory_freezer -from .freeze_impl import issue_freezer -from .freeze_impl import task_freezer -from .freeze_impl import wikipage_freezer - register_freeze_implementation("projects.project", project_freezer) register_freeze_implementation("milestones.milestone", milestone_freezer,) register_freeze_implementation("userstories.userstory", userstory_freezer) @@ -398,13 +401,6 @@ register_freeze_implementation("issues.issue", issue_freezer) register_freeze_implementation("tasks.task", task_freezer) register_freeze_implementation("wiki.wikipage", wikipage_freezer) -from .freeze_impl import project_values -from .freeze_impl import milestone_values -from .freeze_impl import userstory_values -from .freeze_impl import issue_values -from .freeze_impl import task_values -from .freeze_impl import wikipage_values - register_values_implementation("projects.project", project_values) register_values_implementation("milestones.milestone", milestone_values) register_values_implementation("userstories.userstory", userstory_values) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 8da13476..f204fc13 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -34,14 +34,18 @@ from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin +from .utils import attach_extra_info + from . import models from . import services from . import permissions from . import serializers +from . import validators class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): + validator_class = validators.IssueValidator queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) filter_backends = (filters.CanViewIssuesFilterBackend, @@ -145,8 +149,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W def get_queryset(self): qs = super().get_queryset() qs = qs.select_related("owner", "assigned_to", "status", "project") - qs = self.attach_votes_attrs_to_queryset(qs) - return self.attach_watchers_attrs_to_queryset(qs) + qs = attach_extra_info(qs, user=self.request.user) + return qs def pre_save(self, obj): if not obj.id: @@ -181,8 +185,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W def by_ref(self, request): ref = request.QUERY_PARAMS.get("ref", None) project_id = request.QUERY_PARAMS.get("project", None) - issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id) - return self.retrieve(request, pk=issue.pk) + return self.retrieve(request, project_id=project_id, ref=ref) @list_route(methods=["GET"]) def filters_data(self, request, *args, **kwargs): @@ -224,9 +227,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): - serializer = serializers.IssuesBulkSerializer(data=request.DATA) - if serializer.is_valid(): - data = serializer.data + validator = validators.IssuesBulkValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data project = Project.objects.get(pk=data["project_id"]) self.check_permissions(request, 'bulk_create', project) if project.blocked_code is not None: @@ -237,11 +240,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W status=project.default_issue_status, severity=project.default_severity, priority=project.default_priority, type=project.default_issue_type, callback=self.post_save, precall=self.pre_save) + + issues = self.get_queryset().filter(id__in=[i.id for i in issues]) issues_serialized = self.get_serializer_class()(issues, many=True) return response.Ok(data=issues_serialized.data) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet): diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 099171a1..2b773b81 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -17,56 +17,52 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender -from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.serializers import BasicIssueStatusSerializer -from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin - -from taiga.users.serializers import UserBasicInfoSerializer - -from . import models - -import serpy -class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - tags = TagsAndTagsColorsField(default=[], required=False) - external_reference = PgArrayField(required=False) - is_closed = serializers.Field(source="is_closed") - comment = serializers.SerializerMethodField("get_comment") - generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories") - blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") - description_html = serializers.SerializerMethodField("get_description_html") - status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True) - assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) - owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) +class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + severity = Field(attr="severity_id") + priority = Field(attr="priority_id") + type = Field(attr="type_id") + milestone = Field(attr="milestone_id") + project = Field(attr="project_id") + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + external_reference = Field() + version = Field() + watchers = Field() + tags = Field() + is_closed = Field() - class Meta: - model = models.Issue - read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + +class IssueSerializer(IssueListSerializer): + comment = MethodField() + generated_user_stories = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() def get_comment(self, obj): # NOTE: This method and field is necessary to historical comments work return "" def get_generated_user_stories(self, obj): - return [{ - "id": us.id, - "ref": us.ref, - "subject": us.subject, - } for us in obj.generated_user_stories.all()] + assert hasattr(obj, "generated_user_stories_attr"), "instance must have a generated_user_stories_attr attribute" + return obj.generated_user_stories_attr def get_blocked_note_html(self, obj): return mdrender(obj.project, obj.blocked_note) @@ -75,39 +71,5 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa return mdrender(obj.project, obj.description) -class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, - ListStatusExtraInfoSerializerMixin, serializers.LightSerializer): - id = serpy.Field() - ref = serpy.Field() - severity = serpy.Field(attr="severity_id") - priority = serpy.Field(attr="priority_id") - type = serpy.Field(attr="type_id") - milestone = serpy.Field(attr="milestone_id") - project = serpy.Field(attr="project_id") - created_date = serpy.Field() - modified_date = serpy.Field() - finished_date = serpy.Field() - subject = serpy.Field() - external_reference = serpy.Field() - version = serpy.Field() - watchers = serpy.Field() - - class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer): - def serialize_neighbor(self, neighbor): - if neighbor: - return NeighborIssueSerializer(neighbor).data - return None - - -class NeighborIssueSerializer(serializers.ModelSerializer): - class Meta: - model = models.Issue - fields = ("id", "ref", "subject") - depth = 0 - - -class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - bulk_issues = serializers.CharField() + pass diff --git a/taiga/projects/issues/utils.py b/taiga/projects/issues/utils.py new file mode 100644 index 00000000..2053d923 --- /dev/null +++ b/taiga/projects/issues/utils.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + + +def attach_generated_user_stories(queryset, as_field="generated_user_stories_attr"): + """Attach generated user stories json column to each object of the queryset. + + :param queryset: A Django issues queryset object. + :param as_field: Attach the generated user stories as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(t)) + FROM( + SELECT + userstories_userstory.id, + userstories_userstory.ref, + userstories_userstory.subject + FROM userstories_userstory + WHERE generated_from_issue_id = {tbl}.id) t""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_extra_info(queryset, user=None): + queryset = attach_generated_user_stories(queryset) + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/issues/validators.py b/taiga/projects/issues/validators.py new file mode 100644 index 00000000..4c900c15 --- /dev/null +++ b/taiga/projects/issues/validators.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.fields import PgArrayField +from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.validators import ProjectExistsValidator + +from . import models + + +class IssueValidator(WatchersValidator, EditableWatchedResourceSerializer, + validators.ModelValidator): + + tags = TagsAndTagsColorsField(default=[], required=False) + external_reference = PgArrayField(required=False) + + class Meta: + model = models.Issue + read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + + +class IssuesBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_issues = serializers.CharField() diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 1520f2c7..2e0047fc 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -17,7 +17,6 @@ # along with this program. If not, see . from django.apps import apps -from django.db.models import Prefetch from taiga.base import filters from taiga.base import response @@ -31,13 +30,9 @@ from taiga.base.utils.db import get_object_or_none from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin -from taiga.projects.votes.utils import attach_total_voters_to_queryset -from taiga.projects.votes.utils import attach_is_voter_to_queryset -from taiga.projects.notifications.utils import attach_watchers_to_queryset -from taiga.projects.notifications.utils import attach_is_watcher_to_queryset -from taiga.projects.userstories import utils as userstories_utils from . import serializers +from . import validators from . import models from . import permissions from . import utils as milestones_utils @@ -47,6 +42,8 @@ import datetime class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): + serializer_class = serializers.MilestoneSerializer + validator_class = validators.MilestoneValidator permission_classes = (permissions.MilestonePermission,) filter_backends = (filters.CanViewMilestonesFilterBackend,) filter_fields = ( @@ -56,12 +53,6 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ) queryset = models.Milestone.objects.all() - def get_serializer_class(self, *args, **kwargs): - if self.action == "list": - return serializers.MilestoneListSerializer - - return serializers.MilestoneSerializer - def list(self, request, *args, **kwargs): res = super().list(request, *args, **kwargs) self._add_taiga_info_headers() @@ -84,33 +75,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, def get_queryset(self): qs = super().get_queryset() - - # Userstories prefetching - UserStory = apps.get_model("userstories", "UserStory") - - us_qs = UserStory.objects.select_related("milestone", - "project", - "status", - "owner", - "assigned_to", - "generated_from_issue") - - us_qs = userstories_utils.attach_total_points(us_qs) - us_qs = userstories_utils.attach_role_points(us_qs) - us_qs = attach_total_voters_to_queryset(us_qs) - us_qs = self.attach_watchers_attrs_to_queryset(us_qs) - - if self.request.user.is_authenticated(): - us_qs = attach_is_voter_to_queryset(self.request.user, us_qs) - us_qs = attach_is_watcher_to_queryset(us_qs, self.request.user) - - qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs)) - - # Milestones prefetching qs = qs.select_related("project", "owner") - qs = self.attach_watchers_attrs_to_queryset(qs) - qs = milestones_utils.attach_total_points(qs) - qs = milestones_utils.attach_closed_points(qs) + qs = milestones_utils.attach_extra_info(qs, user=self.request.user) qs = qs.order_by("-estimated_start") return qs diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 724126fd..44b3e8f4 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -16,58 +16,29 @@ # 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.base.api import serializers -from taiga.base.utils import json -from taiga.projects.notifications.mixins import WatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin +from taiga.base.fields import Field, MethodField +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.userstories.serializers import UserStoryListSerializer -from . import models -import serpy - - -class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, - ValidateDuplicatedNameInProjectMixin): - total_points = serializers.SerializerMethodField("get_total_points") - closed_points = serializers.SerializerMethodField("get_closed_points") - user_stories = serializers.SerializerMethodField("get_user_stories") - - class Meta: - model = models.Milestone - read_only_fields = ("id", "created_date", "modified_date") - - def get_total_points(self, obj): - return sum(obj.total_points.values()) - - def get_closed_points(self, obj): - return sum(obj.closed_points.values()) - - def get_user_stories(self, obj): - return UserStoryListSerializer(obj.user_stories.all(), many=True).data - - -class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer): - id = serpy.Field() - name = serpy.Field() - slug = serpy.Field() - owner = serpy.Field(attr="owner_id") - project = serpy.Field(attr="project_id") - estimated_start = serpy.Field() - estimated_finish = serpy.Field() - created_date = serpy.Field() - modified_date = serpy.Field() - closed = serpy.Field() - disponibility = serpy.Field() - order = serpy.Field() - watchers = serpy.Field() - user_stories = serpy.MethodField("get_user_stories") - total_points = serpy.MethodField() - closed_points = serpy.MethodField() +class MilestoneSerializer(WatchedResourceSerializer, serializers.LightSerializer): + id = Field() + name = Field() + slug = Field() + owner = Field(attr="owner_id") + project = Field(attr="project_id") + estimated_start = Field() + estimated_finish = Field() + created_date = Field() + modified_date = Field() + closed = Field() + disponibility = Field() + order = Field() + watchers = Field() + user_stories = MethodField() + total_points = MethodField() + closed_points = MethodField() def get_user_stories(self, obj): return UserStoryListSerializer(obj.user_stories.all(), many=True).data diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py index a32d7684..b292b1bd 100644 --- a/taiga/projects/milestones/utils.py +++ b/taiga/projects/milestones/utils.py @@ -17,6 +17,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps +from django.db.models import Prefetch + +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.userstories import utils as userstories_utils +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + def attach_total_points(queryset, as_field="total_points_attr"): """Attach total of point values to each object of the queryset. @@ -28,7 +38,7 @@ def attach_total_points(queryset, as_field="total_points_attr"): """ model = queryset.model sql = """SELECT SUM(projects_points.value) - FROM userstories_rolepoints + FROM userstories_rolepoints INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id WHERE userstories_userstory.milestone_id = {tbl}.id""" @@ -48,7 +58,7 @@ def attach_closed_points(queryset, as_field="closed_points_attr"): """ model = queryset.model sql = """SELECT SUM(projects_points.value) - FROM userstories_rolepoints + FROM userstories_rolepoints INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True""" @@ -56,3 +66,33 @@ def attach_closed_points(queryset, as_field="closed_points_attr"): sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset + + +def attach_extra_info(queryset, user=None): + # Userstories prefetching + UserStory = apps.get_model("userstories", "UserStory") + us_queryset = UserStory.objects.select_related("milestone", + "project", + "status", + "owner", + "assigned_to", + "generated_from_issue") + + us_queryset = userstories_utils.attach_total_points(us_queryset) + us_queryset = userstories_utils.attach_role_points(us_queryset) + us_queryset = attach_total_voters_to_queryset(us_queryset) + us_queryset = attach_watchers_to_queryset(us_queryset) + us_queryset = attach_total_watchers_to_queryset(us_queryset) + us_queryset = attach_is_voter_to_queryset(us_queryset, user) + us_queryset = attach_is_watcher_to_queryset(us_queryset, user) + + queryset = queryset.prefetch_related(Prefetch("user_stories", queryset=us_queryset)) + queryset = attach_total_points(queryset) + queryset = attach_closed_points(queryset) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py index 3648a672..8de3174c 100644 --- a/taiga/projects/milestones/validators.py +++ b/taiga/projects/milestones/validators.py @@ -19,14 +19,23 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.projects.validators import DuplicatedNameInProjectValidator +from taiga.projects.notifications.validators import WatchersValidator from . import models -class SprintExistsValidator: +class MilestoneExistsValidator: def validate_sprint_id(self, attrs, source): value = attrs[source] if not models.Milestone.objects.filter(pk=value).exists(): - msg = _("There's no sprint with that id") + msg = _("There's no milestone with that id") raise serializers.ValidationError(msg) return attrs + + +class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.Milestone + read_only_fields = ("id", "created_date", "modified_date") diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index a47d9bed..945c1119 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -17,34 +17,13 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.users.serializers import ListUserBasicInfoSerializer +from taiga.base.fields import Field, MethodField +from taiga.users.serializers import UserBasicInfoSerializer from django.utils.translation import ugettext as _ -import serpy -class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer): - - def validate_name(self, attrs, source): - """ - Check the points name is not duplicated in the project on creation - """ - model = self.opts.model - qs = None - # If the object exists: - if self.object and attrs.get(source, None): - qs = model.objects.filter(project=self.object.project, name=attrs[source]).exclude(id=self.object.id) - - if not self.object and attrs.get("project", None) and attrs.get(source, None): - qs = model.objects.filter(project=attrs["project"], name=attrs[source]) - - if qs and qs.exists(): - raise serializers.ValidationError(_("Name duplicated for the project")) - - return attrs - - -class ListCachedUsersSerializerMixin(serpy.Serializer): +class CachedUsersSerializerMixin(serializers.LightSerializer): def to_value(self, instance): self._serialized_users = {} return super().to_value(instance) @@ -55,37 +34,40 @@ class ListCachedUsersSerializerMixin(serpy.Serializer): serialized_user = self._serialized_users.get(user.id, None) if serialized_user is None: - serializer_user = ListUserBasicInfoSerializer(user).data - self._serialized_users[user.id] = serializer_user + serialized_user = UserBasicInfoSerializer(user).data + self._serialized_users[user.id] = serialized_user return serialized_user -class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): - owner = serpy.Field(attr="owner_id") - owner_extra_info = serpy.MethodField() +class OwnerExtraInfoSerializerMixin(CachedUsersSerializerMixin): + owner = Field(attr="owner_id") + owner_extra_info = MethodField() def get_owner_extra_info(self, obj): return self.get_user_extra_info(obj.owner) -class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): - assigned_to = serpy.Field(attr="assigned_to_id") - assigned_to_extra_info = serpy.MethodField() +class AssignedToExtraInfoSerializerMixin(CachedUsersSerializerMixin): + assigned_to = Field(attr="assigned_to_id") + assigned_to_extra_info = MethodField() def get_assigned_to_extra_info(self, obj): return self.get_user_extra_info(obj.assigned_to) -class ListStatusExtraInfoSerializerMixin(serpy.Serializer): - status = serpy.Field(attr="status_id") - status_extra_info = serpy.MethodField() +class StatusExtraInfoSerializerMixin(serializers.LightSerializer): + status = Field(attr="status_id") + status_extra_info = MethodField() def to_value(self, instance): self._serialized_status = {} return super().to_value(instance) def get_status_extra_info(self, obj): + if obj.status_id is None: + return None + serialized_status = self._serialized_status.get(obj.status_id, None) if serialized_status is None: serialized_status = { diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 2cad1e97..e9dff950 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import serpy - from functools import partial from operator import is_not @@ -28,16 +26,12 @@ from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import serializers from taiga.base.api.utils import get_object_or_404 -from taiga.base.fields import WatchersField +from taiga.base.fields import WatchersField, MethodField from taiga.projects.notifications import services -from taiga.projects.notifications.utils import (attach_watchers_to_queryset, - attach_is_watcher_to_queryset, - attach_total_watchers_to_queryset) from . serializers import WatcherSerializer - class WatchedResourceMixin: """ Rest Framework resource mixin for resources susceptible @@ -54,14 +48,6 @@ class WatchedResourceMixin: _not_notify = False - def attach_watchers_attrs_to_queryset(self, queryset): - queryset = attach_watchers_to_queryset(queryset) - queryset = attach_total_watchers_to_queryset(queryset) - if self.request.user.is_authenticated(): - queryset = attach_is_watcher_to_queryset(queryset, self.request.user) - - return queryset - @detail_route(methods=["POST"]) def watch(self, request, pk=None): obj = self.get_object() @@ -186,7 +172,10 @@ class WatchedModelMixin(object): return frozenset(filter(is_not_none, participants)) -class BaseWatchedResourceModelSerializer(object): +class WatchedResourceSerializer(serializers.LightSerializer): + is_watcher = MethodField() + total_watchers = MethodField() + def get_is_watcher(self, obj): # The "is_watcher" attribute is attached in the get_queryset of the viewset. if "request" in self.context: @@ -200,28 +189,18 @@ class BaseWatchedResourceModelSerializer(object): return getattr(obj, "total_watchers", 0) or 0 -class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serializers.ModelSerializer): - is_watcher = serializers.SerializerMethodField("get_is_watcher") - total_watchers = serializers.SerializerMethodField("get_total_watchers") - - -class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer): - is_watcher = serpy.MethodField("get_is_watcher") - total_watchers = serpy.MethodField("get_total_watchers") - - -class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): +class EditableWatchedResourceSerializer(serializers.ModelSerializer): watchers = WatchersField(required=False) def restore_object(self, attrs, instance=None): - #watchers is not a field from the model but can be attached in the get_queryset of the viewset. - #If that's the case we need to remove it before calling the super method - watcher_field = self.fields.pop("watchers", None) + # watchers is not a field from the model but can be attached in the get_queryset of the viewset. + # If that's the case we need to remove it before calling the super method + self.fields.pop("watchers", None) self.validate_watchers(attrs, "watchers") new_watcher_ids = attrs.pop("watchers", None) - obj = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance) + obj = super(EditableWatchedResourceSerializer, self).restore_object(attrs, instance) - #A partial update can exclude the watchers field or if the new instance can still not be saved + # A partial update can exclude the watchers field or if the new instance can still not be saved if instance is None or new_watcher_ids is None: return obj @@ -230,7 +209,6 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids)) removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids)) - User = get_user_model() adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids) removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids) for user in adding_users: @@ -244,7 +222,7 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): return obj def to_native(self, obj): - #if watchers wasn't attached via the get_queryset of the viewset we need to manually add it + # if watchers wasn't attached via the get_queryset of the viewset we need to manually add it if obj is not None: if not hasattr(obj, "watchers"): obj.watchers = [user.id for user in obj.get_watchers()] @@ -254,10 +232,10 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): if user and user.is_authenticated(): obj.is_watcher = user.id in obj.watchers - return super(WatchedResourceModelSerializer, self).to_native(obj) + return super(WatchedResourceSerializer, self).to_native(obj) def save(self, **kwargs): - obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs) + obj = super(EditableWatchedResourceSerializer, self).save(**kwargs) self.fields["watchers"] = WatchersField(required=False) obj.watchers = [user.id for user in obj.get_watchers()] return obj diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py index 00b98d63..ae6bd34c 100644 --- a/taiga/projects/notifications/utils.py +++ b/taiga/projects/notifications/utils.py @@ -53,15 +53,18 @@ def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"): """ model = queryset.model type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) - sql = ("""SELECT CASE WHEN (SELECT count(*) - FROM notifications_watched - WHERE notifications_watched.content_type_id = {type_id} - AND notifications_watched.object_id = {tbl}.id - AND notifications_watched.user_id = {user_id}) > 0 - THEN TRUE - ELSE FALSE - END""") - sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + if user is None or user.is_anonymous(): + sql = """SELECT false""" + else: + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM notifications_watched + WHERE notifications_watched.content_type_id = {type_id} + AND notifications_watched.object_id = {tbl}.id + AND notifications_watched.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) qs = queryset.extra(select={as_field: sql}) return qs diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index c10a4810..96b8d0ba 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,135 +16,121 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import serpy - from django.utils.translation import ugettext as _ -from django.db.models import Q from taiga.base.api import serializers -from taiga.base.fields import JsonField -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField, I18NField from taiga.permissions import services as permissions_services from taiga.users.services import get_photo_or_gravatar_url from taiga.users.serializers import UserBasicInfoSerializer -from taiga.users.serializers import ProjectRoleSerializer -from taiga.users.serializers import ListUserBasicInfoSerializer -from taiga.users.validators import RoleExistsValidator -from taiga.permissions.services import get_user_project_permissions from taiga.permissions.services import calculate_permissions from taiga.permissions.services import is_project_admin, is_project_owner -from . import models from . import services -from .custom_attributes.serializers import UserStoryCustomAttributeSerializer -from .custom_attributes.serializers import TaskCustomAttributeSerializer -from .custom_attributes.serializers import IssueCustomAttributeSerializer -from .likes.mixins.serializers import FanResourceSerializerMixin -from .mixins.serializers import ValidateDuplicatedNameInProjectMixin from .notifications.choices import NotifyLevel -from .notifications.mixins import WatchedResourceModelSerializer -from .tagging.fields import TagsField -from .tagging.fields import TagsColorsField -from .validators import ProjectExistsValidator - -import serpy - -###################################################### -## Custom values for selectors -###################################################### - -class PointsSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.Points - i18n_fields = ("name",) - - -class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.UserStoryStatus - i18n_fields = ("name",) - - -class BasicUserStoryStatusSerializer(serializers.ModelSerializer): - class Meta: - model = models.UserStoryStatus - i18n_fields = ("name",) - fields = ("name", "color") - - -class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.TaskStatus - i18n_fields = ("name",) - - -class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer): - class Meta: - model = models.TaskStatus - i18n_fields = ("name",) - fields = ("name", "color") - - -class SeveritySerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.Severity - i18n_fields = ("name",) - - -class PrioritySerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.Priority - i18n_fields = ("name",) - - -class IssueStatusSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.IssueStatus - i18n_fields = ("name",) - - -class BasicIssueStatusSerializer(serializers.ModelSerializer): - class Meta: - model = models.IssueStatus - i18n_fields = ("name",) - fields = ("name", "color") - - -class IssueTypeSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.IssueType - i18n_fields = ("name",) ###################################################### -## Members +# Custom values for selectors ###################################################### -class MembershipSerializer(serializers.ModelSerializer): - role_name = serializers.CharField(source='role.name', required=False, read_only=True, i18n=True) - full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True) - user_email = serializers.EmailField(source='user.email', required=False, read_only=True) - is_user_active = serializers.BooleanField(source='user.is_active', required=False, - read_only=True) - email = serializers.EmailField(required=True) - color = serializers.CharField(source='user.color', required=False, read_only=True) - photo = serializers.SerializerMethodField("get_photo") - project_name = serializers.SerializerMethodField("get_project_name") - project_slug = serializers.SerializerMethodField("get_project_slug") - invited_by = UserBasicInfoSerializer(read_only=True) - is_owner = serializers.SerializerMethodField("get_is_owner") +class PointsSerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + value = Field() + project = Field(attr="project_id") - class Meta: - model = models.Membership - # IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date - # with this info (excluding here user_email and email) - read_only_fields = ("user",) - exclude = ("token", "user_email", "email") - def get_photo(self, project): - return get_photo_or_gravatar_url(project.user) +class UserStoryStatusSerializer(serializers.LightSerializer): + name = I18NField() + slug = Field() + order = Field() + is_closed = Field() + is_archived = Field() + color = Field() + wip_limit = Field() + project = Field(attr="project_id") + + +class TaskStatusSerializer(serializers.LightSerializer): + name = I18NField() + slug = Field() + order = Field() + is_closed = Field() + color = Field() + project = Field(attr="project_id") + + +class SeveritySerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + color = Field() + project = Field(attr="project_id") + + +class PrioritySerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + color = Field() + project = Field(attr="project_id") + + +class IssueStatusSerializer(serializers.LightSerializer): + name = I18NField() + slug = Field() + order = Field() + is_closed = Field() + color = Field() + project = Field(attr="project_id") + + +class IssueTypeSerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + color = Field() + project = Field(attr="project_id") + + +###################################################### +# Members +###################################################### + +class MembershipSerializer(serializers.LightSerializer): + id = Field() + user = Field(attr="user_id") + project = Field(attr="project_id") + role = Field(attr="role_id") + is_admin = Field() + created_at = Field() + invited_by = Field(attr="invited_by_id") + invitation_extra_text = Field() + user_order = Field() + role_name = MethodField() + full_name = MethodField() + is_user_active = MethodField() + color = MethodField() + photo = MethodField() + project_name = MethodField() + project_slug = MethodField() + invited_by = UserBasicInfoSerializer() + is_owner = MethodField() + + def get_role_name(self, obj): + return obj.role.name if obj.role else None + + def get_full_name(self, obj): + return obj.user.get_full_name() if obj.user else None + + def get_is_user_active(self, obj): + return obj.user.is_active if obj.user else False + + def get_color(self, obj): + return obj.user.color if obj.user else None + + def get_photo(self, obj): + return get_photo_or_gravatar_url(obj.user) def get_project_name(self, obj): return obj.project.name if obj and obj.project else "" @@ -156,230 +142,84 @@ class MembershipSerializer(serializers.ModelSerializer): return (obj and obj.user_id and obj.project_id and obj.project.owner_id and obj.user_id == obj.project.owner_id) - def validate_email(self, attrs, source): - project = attrs.get("project", None) - if project is None: - project = self.object.project - - email = attrs[source] - - qs = models.Membership.objects.all() - - # If self.object is not None, the serializer is in update - # mode, and for it, it should exclude self. - if self.object: - qs = qs.exclude(pk=self.object.pk) - - qs = qs.filter(Q(project_id=project.id, user__email=email) | - Q(project_id=project.id, email=email)) - - if qs.count() > 0: - raise serializers.ValidationError(_("Email address is already taken")) - - return attrs - - def validate_role(self, attrs, source): - project = attrs.get("project", None) - if project is None: - project = self.object.project - - role = attrs[source] - - if project.roles.filter(id=role.id).count() == 0: - raise serializers.ValidationError(_("Invalid role for the project")) - - return attrs - - def validate_is_admin(self, attrs, source): - project = attrs.get("project", None) - if project is None: - project = self.object.project - - if (self.object and self.object.user): - if self.object.user.id == project.owner_id and attrs[source] != True: - raise serializers.ValidationError(_("The project owner must be admin.")) - - if not services.project_has_valid_admins(project, exclude_user=self.object.user): - raise serializers.ValidationError(_("At least one user must be an active admin for this project.")) - - return attrs - class MembershipAdminSerializer(MembershipSerializer): - class Meta: - model = models.Membership - # IMPORTANT: Maintain the MembershipSerializer Meta up to date - # with this info (excluding there user_email and email) - read_only_fields = ("user",) - exclude = ("token",) + email = Field() + user_email = MethodField() + def get_user_email(self, obj): + return obj.user.email if obj.user else None -class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer): - email = serializers.EmailField() - role_id = serializers.IntegerField() - - -class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - bulk_memberships = MemberBulkSerializer(many=True) - invitation_extra_text = serializers.CharField(required=False, max_length=255) - - -class ProjectMemberSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(source="user.id", read_only=True) - username = serializers.CharField(source='user.username', read_only=True) - full_name = serializers.CharField(source='user.full_name', read_only=True) - full_name_display = serializers.CharField(source='user.get_full_name', read_only=True) - color = serializers.CharField(source='user.color', read_only=True) - photo = serializers.SerializerMethodField("get_photo") - is_active = serializers.BooleanField(source='user.is_active', read_only=True) - role_name = serializers.CharField(source='role.name', read_only=True, i18n=True) - - class Meta: - model = models.Membership - exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text", - "user_order") - - def get_photo(self, membership): - return get_photo_or_gravatar_url(membership.user) + # IMPORTANT: Maintain the MembershipSerializer Meta up to date + # with this info (excluding there user_email and email) ###################################################### -## Projects +# Projects ###################################################### -class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer, - serializers.ModelSerializer): - anon_permissions = PgArrayField(required=False) - public_permissions = PgArrayField(required=False) - my_permissions = serializers.SerializerMethodField("get_my_permissions") +class ProjectSerializer(serializers.LightSerializer): + id = Field() + name = Field() + slug = Field() + description = Field() + created_date = Field() + modified_date = Field() + owner = MethodField() + members = MethodField() + total_milestones = Field() + total_story_points = Field() + is_backlog_activated = Field() + is_kanban_activated = Field() + is_wiki_activated = Field() + is_issues_activated = Field() + videoconferences = Field() + videoconferences_extra_data = Field() + creation_template = Field(attr="creation_template_id") + is_private = Field() + anon_permissions = Field() + public_permissions = Field() + is_featured = Field() + is_looking_for_people = Field() + looking_for_people_note = Field() + blocked_code = Field() + totals_updated_datetime = Field() + total_fans = Field() + total_fans_last_week = Field() + total_fans_last_month = Field() + total_fans_last_year = Field() + total_activity = Field() + total_activity_last_week = Field() + total_activity_last_month = Field() + total_activity_last_year = Field() - owner = UserBasicInfoSerializer(read_only=True) - i_am_owner = serializers.SerializerMethodField("get_i_am_owner") - i_am_admin = serializers.SerializerMethodField("get_i_am_admin") - i_am_member = serializers.SerializerMethodField("get_i_am_member") + tags = Field() + tags_colors = MethodField() - tags = TagsField(default=[], required=False) - tags_colors = TagsColorsField(required=False, read_only=True) + default_points = Field(attr="default_points_id") + default_us_status = Field(attr="default_us_status_id") + default_task_status = Field(attr="default_task_status_id") + default_priority = Field(attr="default_priority_id") + default_severity = Field(attr="default_severity_id") + default_issue_status = Field(attr="default_issue_status_id") + default_issue_type = Field(attr="default_issue_type_id") - notify_level = serializers.SerializerMethodField("get_notify_level") - total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones") - total_watchers = serializers.SerializerMethodField("get_total_watchers") + my_permissions = MethodField() - logo_small_url = serializers.SerializerMethodField("get_logo_small_url") - logo_big_url = serializers.SerializerMethodField("get_logo_big_url") + i_am_owner = MethodField() + i_am_admin = MethodField() + i_am_member = MethodField() - class Meta: - model = models.Project - read_only_fields = ("created_date", "modified_date", "slug", "blocked_code") - exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref", - "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid", - "transfer_token") + notify_level = MethodField() + total_closed_milestones = MethodField() - def get_my_permissions(self, obj): - if "request" in self.context: - return get_user_project_permissions(self.context["request"].user, obj) - return [] + is_watcher = MethodField() + total_watchers = MethodField() - def get_i_am_owner(self, obj): - if "request" in self.context: - return is_project_owner(self.context["request"].user, obj) - return False + logo_small_url = MethodField() + logo_big_url = MethodField() - def get_i_am_admin(self, obj): - if "request" in self.context: - return is_project_admin(self.context["request"].user, obj) - return False - - def get_i_am_member(self, obj): - if "request" in self.context: - user = self.context["request"].user - if not user.is_anonymous() and user.cached_membership_for_project(obj): - return True - return False - - def get_total_closed_milestones(self, obj): - return obj.milestones.filter(closed=True).count() - - def get_notify_level(self, obj): - if "request" in self.context: - user = self.context["request"].user - return user.is_authenticated() and user.get_notify_level(obj) - - return None - - def get_total_watchers(self, obj): - return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count() - - def get_logo_small_url(self, obj): - return services.get_logo_small_thumbnail_url(obj) - - def get_logo_big_url(self, obj): - return services.get_logo_big_thumbnail_url(obj) - - -class LightProjectSerializer(serializers.LightSerializer): - id = serpy.Field() - name = serpy.Field() - slug = serpy.Field() - description = serpy.Field() - created_date = serpy.Field() - modified_date = serpy.Field() - owner = serpy.MethodField() - members = serpy.MethodField() - total_milestones = serpy.Field() - total_story_points = serpy.Field() - is_backlog_activated = serpy.Field() - is_kanban_activated = serpy.Field() - is_wiki_activated = serpy.Field() - is_issues_activated = serpy.Field() - videoconferences = serpy.Field() - videoconferences_extra_data = serpy.Field() - creation_template = serpy.Field(attr="creation_template_id") - is_private = serpy.Field() - anon_permissions = serpy.Field() - public_permissions = serpy.Field() - is_featured = serpy.Field() - is_looking_for_people = serpy.Field() - looking_for_people_note = serpy.Field() - blocked_code = serpy.Field() - totals_updated_datetime = serpy.Field() - total_fans = serpy.Field() - total_fans_last_week = serpy.Field() - total_fans_last_month = serpy.Field() - total_fans_last_year = serpy.Field() - total_activity = serpy.Field() - total_activity_last_week = serpy.Field() - total_activity_last_month = serpy.Field() - total_activity_last_year = serpy.Field() - - tags = serpy.Field() - tags_colors = serpy.MethodField() - - default_points = serpy.Field(attr="default_points_id") - default_us_status = serpy.Field(attr="default_us_status_id") - default_task_status = serpy.Field(attr="default_task_status_id") - default_priority = serpy.Field(attr="default_priority_id") - default_severity = serpy.Field(attr="default_severity_id") - default_issue_status = serpy.Field(attr="default_issue_status_id") - default_issue_type = serpy.Field(attr="default_issue_type_id") - - my_permissions = serpy.MethodField() - - i_am_owner = serpy.MethodField() - i_am_admin = serpy.MethodField() - i_am_member = serpy.MethodField() - - notify_level = serpy.MethodField("get_notify_level") - total_closed_milestones = serpy.MethodField() - - is_watcher = serpy.MethodField() - total_watchers = serpy.MethodField() - - logo_small_url = serpy.MethodField() - logo_big_url = serpy.MethodField() - - is_fan = serpy.Field(attr="is_fan_attr") + is_fan = Field(attr="is_fan_attr") def get_members(self, obj): assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute" @@ -395,7 +235,8 @@ class LightProjectSerializer(serializers.LightSerializer): if "request" in self.context: user = self.context["request"].user - if not user.is_anonymous() and user.id in [m.get("id") for m in obj.members_attr if m["id"] is not None]: + user_ids = [m.get("id") for m in obj.members_attr if m["id"] is not None] + if not user.is_anonymous() and user.id in user_ids: return True return False @@ -407,17 +248,17 @@ class LightProjectSerializer(serializers.LightSerializer): if "request" in self.context: user = self.context["request"].user return calculate_permissions( - is_authenticated = user.is_authenticated(), - is_superuser = user.is_superuser, - is_member = self.get_i_am_member(obj), - is_admin = self.get_i_am_admin(obj), - role_permissions = obj.my_role_permissions_attr, - anon_permissions = obj.anon_permissions, - public_permissions = obj.public_permissions) + is_authenticated=user.is_authenticated(), + is_superuser=user.is_superuser, + is_member=self.get_i_am_member(obj), + is_admin=self.get_i_am_admin(obj), + role_permissions=obj.my_role_permissions_attr, + anon_permissions=obj.anon_permissions, + public_permissions=obj.public_permissions) return [] def get_owner(self, obj): - return ListUserBasicInfoSerializer(obj.owner).data + return UserBasicInfoSerializer(obj.owner).data def get_i_am_owner(self, obj): if "request" in self.context: @@ -436,7 +277,7 @@ class LightProjectSerializer(serializers.LightSerializer): def get_is_watcher(self, obj): assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute" np = self.get_notify_level(obj) - return np != None and np != NotifyLevel.none + return np is not None and np != NotifyLevel.none def get_total_watchers(self, obj): assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute" @@ -466,36 +307,36 @@ class LightProjectSerializer(serializers.LightSerializer): return services.get_logo_big_thumbnail_url(obj) -class LightProjectDetailSerializer(LightProjectSerializer): - us_statuses = serpy.Field(attr="userstory_statuses_attr") - points = serpy.Field(attr="points_attr") - task_statuses = serpy.Field(attr="task_statuses_attr") - issue_statuses = serpy.Field(attr="issue_statuses_attr") - issue_types = serpy.Field(attr="issue_types_attr") - priorities = serpy.Field(attr="priorities_attr") - severities = serpy.Field(attr="severities_attr") - userstory_custom_attributes = serpy.Field(attr="userstory_custom_attributes_attr") - task_custom_attributes = serpy.Field(attr="task_custom_attributes_attr") - issue_custom_attributes = serpy.Field(attr="issue_custom_attributes_attr") - roles = serpy.Field(attr="roles_attr") - members = serpy.MethodField() - total_memberships = serpy.MethodField() - is_out_of_owner_limits = serpy.MethodField() +class ProjectDetailSerializer(ProjectSerializer): + us_statuses = Field(attr="userstory_statuses_attr") + points = Field(attr="points_attr") + task_statuses = Field(attr="task_statuses_attr") + issue_statuses = Field(attr="issue_statuses_attr") + issue_types = Field(attr="issue_types_attr") + priorities = Field(attr="priorities_attr") + severities = Field(attr="severities_attr") + userstory_custom_attributes = Field(attr="userstory_custom_attributes_attr") + task_custom_attributes = Field(attr="task_custom_attributes_attr") + issue_custom_attributes = Field(attr="issue_custom_attributes_attr") + roles = Field(attr="roles_attr") + members = MethodField() + total_memberships = MethodField() + is_out_of_owner_limits = MethodField() - #Admin fields - is_private_extra_info = serpy.MethodField() - max_memberships = serpy.MethodField() - issues_csv_uuid = serpy.Field() - tasks_csv_uuid = serpy.Field() - userstories_csv_uuid = serpy.Field() - transfer_token = serpy.Field() + # Admin fields + is_private_extra_info = MethodField() + max_memberships = MethodField() + issues_csv_uuid = Field() + tasks_csv_uuid = Field() + userstories_csv_uuid = Field() + transfer_token = Field() def to_value(self, instance): # Name attributes must be translated - for attr in ["userstory_statuses_attr","points_attr", "task_statuses_attr", + for attr in ["userstory_statuses_attr", "points_attr", "task_statuses_attr", "issue_statuses_attr", "issue_types_attr", "priorities_attr", "severities_attr", "userstory_custom_attributes_attr", - "task_custom_attributes_attr","issue_custom_attributes_attr", "roles_attr"]: + "task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]: assert hasattr(instance, attr), "instance must have a {} attribute".format(attr) val = getattr(instance, attr) @@ -547,8 +388,9 @@ class LightProjectDetailSerializer(LightProjectSerializer): def get_is_out_of_owner_limits(self, obj): assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" - return services.check_if_project_is_out_of_owner_limits(obj, - current_memberships = self.get_total_memberships(obj), + return services.check_if_project_is_out_of_owner_limits( + obj, + current_memberships=self.get_total_memberships(obj), current_private_projects=obj.private_projects_same_owner_attr, current_public_projects=obj.public_projects_same_owner_attr ) @@ -556,8 +398,9 @@ class LightProjectDetailSerializer(LightProjectSerializer): def get_is_private_extra_info(self, obj): assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" - return services.check_if_project_privacity_can_be_changed(obj, - current_memberships = self.get_total_memberships(obj), + return services.check_if_project_privacity_can_be_changed( + obj, + current_memberships=self.get_total_memberships(obj), current_private_projects=obj.private_projects_same_owner_attr, current_public_projects=obj.public_projects_same_owner_attr ) @@ -565,41 +408,32 @@ class LightProjectDetailSerializer(LightProjectSerializer): def get_max_memberships(self, obj): return services.get_max_memberships_for_project(obj) -###################################################### -## Liked -###################################################### - -class LikedSerializer(serializers.ModelSerializer): - class Meta: - model = models.Project - fields = ['id', 'name', 'slug'] - - ###################################################### -## Project Templates +# Project Templates ###################################################### -class ProjectTemplateSerializer(serializers.ModelSerializer): - default_options = JsonField(required=False, label=_("Default options")) - us_statuses = JsonField(required=False, label=_("User story's statuses")) - points = JsonField(required=False, label=_("Points")) - task_statuses = JsonField(required=False, label=_("Task's statuses")) - issue_statuses = JsonField(required=False, label=_("Issue's statuses")) - issue_types = JsonField(required=False, label=_("Issue's types")) - priorities = JsonField(required=False, label=_("Priorities")) - severities = JsonField(required=False, label=_("Severities")) - roles = JsonField(required=False, label=_("Roles")) - - class Meta: - model = models.ProjectTemplate - read_only_fields = ("created_date", "modified_date") - i18n_fields = ("name", "description") - -###################################################### -## Project order bulk serializers -###################################################### - -class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - order = serializers.IntegerField() +class ProjectTemplateSerializer(serializers.LightSerializer): + id = Field() + name = I18NField() + slug = Field() + description = I18NField() + order = Field() + created_date = Field() + modified_date = Field() + default_owner_role = Field() + is_backlog_activated = Field() + is_kanban_activated = Field() + is_wiki_activated = Field() + is_issues_activated = Field() + videoconferences = Field() + videoconferences_extra_data = Field() + default_options = Field() + us_statuses = Field() + points = Field() + task_statuses = Field() + issue_statuses = Field() + issue_types = Field() + priorities = Field() + severities = Field() + roles = Field() diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 01ae057e..bec134c5 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -26,7 +26,6 @@ from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.projects.attachments.utils import attach_basic_attachments from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, TaskStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin @@ -38,10 +37,14 @@ from . import models from . import permissions from . import serializers from . import services +from . import validators +from . import utils as tasks_utils -class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): +class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, + WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, + ModelCrudViewSet): + validator_class = validators.TaskValidator queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, @@ -74,17 +77,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa def get_queryset(self): qs = super().get_queryset() - qs = self.attach_votes_attrs_to_queryset(qs) qs = qs.select_related("milestone", "project", "status", "owner", "assigned_to") - qs = self.attach_watchers_attrs_to_queryset(qs) - if "include_attachments" in self.request.QUERY_PARAMS: - qs = attach_basic_attachments(qs) - qs = qs.extra(select={"include_attachments": "True"}) + include_attachments = "include_attachments" in self.request.QUERY_PARAMS + qs = tasks_utils.attach_extra_info(qs, user=self.request.user, + include_attachments=include_attachments) return qs @@ -164,8 +165,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa def by_ref(self, request): ref = request.QUERY_PARAMS.get("ref", None) project_id = request.QUERY_PARAMS.get("project", None) - task = get_object_or_404(models.Task, ref=ref, project_id=project_id) - return self.retrieve(request, pk=task.pk) + return self.retrieve(request, project_id=project_id, ref=ref) @list_route(methods=["GET"]) def csv(self, request): @@ -182,9 +182,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): - serializer = serializers.TasksBulkSerializer(data=request.DATA) - if serializer.is_valid(): - data = serializer.data + validator = validators.TasksBulkValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data project = Project.objects.get(id=data["project_id"]) self.check_permissions(request, 'bulk_create', project) if project.blocked_code is not None: @@ -194,18 +194,20 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], status_id=data.get("status_id") or project.default_task_status_id, project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) + + tasks = self.get_queryset().filter(id__in=[i.id for i in tasks]) tasks_serialized = self.get_serializer_class()(tasks, many=True) return response.Ok(tasks_serialized.data) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) def _bulk_update_order(self, order_field, request, **kwargs): - serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data project = get_object_or_404(Project, pk=data["project_id"]) self.check_permissions(request, "bulk_update_order", project) diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index ac82c570..cd649424 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -16,101 +16,44 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ - from taiga.base.api import serializers -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender -from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin -from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.serializers import BasicTaskStatusSerializerSerializer -from taiga.mdrender.service import render as mdrender -from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.tasks.validators import TaskExistsValidator -from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin - -from taiga.users.serializers import UserBasicInfoSerializer -from taiga.users.services import get_photo_or_gravatar_url -from taiga.users.services import get_big_photo_or_gravatar_url - -from . import models - -import serpy -class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - - tags = TagsAndTagsColorsField(default=[], required=False) - external_reference = PgArrayField(required=False) - comment = serializers.SerializerMethodField("get_comment") - milestone_slug = serializers.SerializerMethodField("get_milestone_slug") - blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") - description_html = serializers.SerializerMethodField("get_description_html") - is_closed = serializers.SerializerMethodField("get_is_closed") - status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True) - assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) - owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) - - class Meta: - model = models.Task - read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') - - def get_comment(self, obj): - return "" - - def get_milestone_slug(self, obj): - if obj.milestone: - return obj.milestone.slug - else: - return None - - def get_blocked_note_html(self, obj): - return mdrender(obj.project, obj.blocked_note) - - def get_description_html(self, obj): - return mdrender(obj.project, obj.description) - - def get_is_closed(self, obj): - return obj.status is not None and obj.status.is_closed - - -class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, - ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin, +class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, serializers.LightSerializer): - id = serpy.Field() - user_story = serpy.Field(attr="user_story_id") - ref = serpy.Field() - project = serpy.Field(attr="project_id") - milestone = serpy.Field(attr="milestone_id") - milestone_slug = serpy.MethodField("get_milestone_slug") - created_date = serpy.Field() - modified_date = serpy.Field() - finished_date = serpy.Field() - subject = serpy.Field() - us_order = serpy.Field() - taskboard_order = serpy.Field() - is_iocaine = serpy.Field() - external_reference = serpy.Field() - version = serpy.Field() - watchers = serpy.Field() - is_blocked = serpy.Field() - blocked_note = serpy.Field() - tags = serpy.Field() - is_closed = serpy.MethodField() + id = Field() + user_story = Field(attr="user_story_id") + ref = Field() + project = Field(attr="project_id") + milestone = Field(attr="milestone_id") + milestone_slug = MethodField() + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + us_order = Field() + taskboard_order = Field() + is_iocaine = Field() + external_reference = Field() + version = Field() + watchers = Field() + is_blocked = Field() + blocked_note = Field() + tags = Field() + is_closed = MethodField() def get_milestone_slug(self, obj): return obj.milestone.slug if obj.milestone else None @@ -119,36 +62,21 @@ class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceMod return obj.status is not None and obj.status.is_closed +class TaskSerializer(TaskListSerializer): + comment = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() + + def get_comment(self, obj): + return "" + + def get_blocked_note_html(self, obj): + return mdrender(obj.project, obj.blocked_note) + + def get_description_html(self, obj): + return mdrender(obj.project, obj.description) + + class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer): - def serialize_neighbor(self, neighbor): - if neighbor: - return NeighborTaskSerializer(neighbor).data - return None - - -class NeighborTaskSerializer(serializers.ModelSerializer): - class Meta: - model = models.Task - fields = ("id", "ref", "subject") - depth = 0 - - -class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, - TaskExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - sprint_id = serializers.IntegerField() - status_id = serializers.IntegerField(required=False) - us_id = serializers.IntegerField(required=False) - bulk_tasks = serializers.CharField() - - -## Order bulk serializers - -class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer): - task_id = serializers.IntegerField() - order = serializers.IntegerField() - - -class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - bulk_tasks = _TaskOrderBulkSerializer(many=True) + pass diff --git a/taiga/projects/tasks/utils.py b/taiga/projects/tasks/utils.py new file mode 100644 index 00000000..d10dddab --- /dev/null +++ b/taiga/projects/tasks/utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 taiga.projects.attachments.utils import attach_basic_attachments +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False): + + if include_attachments: + queryset = attach_basic_attachments(queryset) + queryset = queryset.extra(select={"include_attachments": "True"}) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index 4a100779..7f71636c 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -19,7 +19,13 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers - +from taiga.base.api import validators +from taiga.base.fields import PgArrayField +from taiga.projects.milestones.validators import MilestoneExistsValidator +from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.validators import ProjectExistsValidator from . import models @@ -30,3 +36,33 @@ class TaskExistsValidator: msg = _("There's no task with that id") raise serializers.ValidationError(msg) return attrs + + +class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): + tags = TagsAndTagsColorsField(default=[], required=False) + external_reference = PgArrayField(required=False) + + class Meta: + model = models.Task + read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + + +class TasksBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, + TaskExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + sprint_id = serializers.IntegerField() + status_id = serializers.IntegerField(required=False) + us_id = serializers.IntegerField(required=False) + bulk_tasks = serializers.CharField() + + +# Order bulk validators + +class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator): + task_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_tasks = _TaskOrderBulkValidator(many=True) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 87ecf18b..47487e88 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -16,12 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from contextlib import closing -from collections import namedtuple - from django.apps import apps -from django.db import transaction, connection -from django.db.models.sql import datastructures +from django.db import transaction from django.utils.translation import ugettext as _ from django.http import HttpResponse @@ -36,7 +32,6 @@ from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet from taiga.base.api.utils import get_object_or_404 -from taiga.projects.attachments.utils import attach_basic_attachments from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.services import take_snapshot from taiga.projects.milestones.models import Milestone @@ -45,21 +40,20 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin -from taiga.projects.userstories.models import RolePoints from taiga.projects.votes.mixins.viewsets import VotedResourceMixin from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin -from taiga.projects.userstories.utils import attach_total_points -from taiga.projects.userstories.utils import attach_role_points -from taiga.projects.userstories.utils import attach_tasks +from taiga.projects.userstories.utils import attach_extra_info from . import models from . import permissions from . import serializers from . import services +from . import validators class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): + validator_class = validators.UserStoryValidator queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) filter_backends = (filters.CanViewUsFilterBackend, @@ -105,18 +99,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "assigned_to", "generated_from_issue") - qs = self.attach_votes_attrs_to_queryset(qs) - qs = self.attach_watchers_attrs_to_queryset(qs) - qs = attach_total_points(qs) - qs = attach_role_points(qs) - - if "include_attachments" in self.request.QUERY_PARAMS: - qs = attach_basic_attachments(qs) - qs = qs.extra(select={"include_attachments": "True"}) - - if "include_tasks" in self.request.QUERY_PARAMS: - qs = attach_tasks(qs) - qs = qs.extra(select={"include_tasks": "True"}) + include_attachments = "include_attachments" in self.request.QUERY_PARAMS + include_tasks = "include_tasks" in self.request.QUERY_PARAMS + qs = attach_extra_info(qs, user=self.request.user, + include_attachments=include_attachments, + include_tasks=include_tasks) return qs @@ -239,8 +226,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def by_ref(self, request): ref = request.QUERY_PARAMS.get("ref", None) project_id = request.QUERY_PARAMS.get("project", None) - userstory = get_object_or_404(models.UserStory, ref=ref, project_id=project_id) - return self.retrieve(request, pk=userstory.pk) + return self.retrieve(request, project_id=project_id, ref=ref) @list_route(methods=["GET"]) def csv(self, request): @@ -257,9 +243,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): - serializer = serializers.UserStoriesBulkSerializer(data=request.DATA) - if serializer.is_valid(): - data = serializer.data + validator = validators.UserStoriesBulkValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data project = Project.objects.get(id=data["project_id"]) self.check_permissions(request, 'bulk_create', project) if project.blocked_code is not None: @@ -269,17 +255,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi data["bulk_stories"], project=project, owner=request.user, status_id=data.get("status_id") or project.default_us_status_id, callback=self.post_save, precall=self.pre_save) + + user_stories = self.get_queryset().filter(id__in=[i.id for i in user_stories]) user_stories_serialized = self.get_serializer_class()(user_stories, many=True) + return response.Ok(user_stories_serialized.data) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) @list_route(methods=["POST"]) def bulk_update_milestone(self, request, **kwargs): - serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.UpdateMilestoneBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data project = get_object_or_404(Project, pk=data["project_id"]) milestone = get_object_or_404(Milestone, pk=data["milestone_id"]) @@ -291,11 +280,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return response.NoContent() def _bulk_update_order(self, order_field, request, **kwargs): - serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.UpdateUserStoriesOrderBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data project = get_object_or_404(Project, pk=data["project_id"]) self.check_permissions(request, "bulk_update_order", project) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 236a54d8..ef15eec6 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -16,96 +16,111 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from collections import ChainMap - -from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ - from taiga.base.api import serializers -from taiga.base.api.utils import get_object_or_404 -from taiga.base.fields import PickledObjectField -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField from taiga.base.neighbors import NeighborsSerializerMixin -from taiga.base.utils import json from taiga.mdrender.service import render as mdrender -from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin -from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin -from taiga.projects.models import Project, UserStoryStatus -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.serializers import BasicUserStoryStatusSerializer -from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.userstories.validators import UserStoryExistsValidator -from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin - -from taiga.users.serializers import UserBasicInfoSerializer -from taiga.users.serializers import ListUserBasicInfoSerializer -from taiga.users.services import get_photo_or_gravatar_url -from taiga.users.services import get_big_photo_or_gravatar_url - -from . import models - -import serpy -class RolePointsField(serializers.WritableField): - def to_native(self, obj): - return {str(o.role.id): o.points.id for o in obj.all()} +class OriginIssueSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() - def from_native(self, obj): - if isinstance(obj, dict): - return obj - return json.loads(obj) + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) -class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, - EditableWatchedResourceModelSerializer, serializers.ModelSerializer): - tags = TagsAndTagsColorsField(default=[], required=False) - external_reference = PgArrayField(required=False) - points = RolePointsField(source="role_points", required=False) - total_points = serializers.SerializerMethodField("get_total_points") - comment = serializers.SerializerMethodField("get_comment") - milestone_slug = serializers.SerializerMethodField("get_milestone_slug") - milestone_name = serializers.SerializerMethodField("get_milestone_name") - origin_issue = serializers.SerializerMethodField("get_origin_issue") - blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") - description_html = serializers.SerializerMethodField("get_description_html") - status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True) - assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) - owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) - tribe_gig = PickledObjectField(required=False) +class UserStoryListSerializer( + VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, + serializers.LightSerializer): - class Meta: - model = models.UserStory - depth = 0 - read_only_fields = ('created_date', 'modified_date', 'owner') + id = Field() + ref = Field() + milestone = Field(attr="milestone_id") + milestone_slug = MethodField() + milestone_name = MethodField() + project = Field(attr="project_id") + is_closed = Field() + points = MethodField() + backlog_order = Field() + sprint_order = Field() + kanban_order = Field() + created_date = Field() + modified_date = Field() + finish_date = Field() + subject = Field() + client_requirement = Field() + team_requirement = Field() + generated_from_issue = Field(attr="generated_from_issue_id") + external_reference = Field() + tribe_gig = Field() + version = Field() + watchers = Field() + is_blocked = Field() + blocked_note = Field() + tags = Field() + total_points = MethodField() + comment = MethodField() + origin_issue = OriginIssueSerializer(attr="generated_from_issue") + + tasks = MethodField() + + def get_milestone_slug(self, obj): + return obj.milestone.slug if obj.milestone else None + + def get_milestone_name(self, obj): + return obj.milestone.name if obj.milestone else None def get_total_points(self, obj): - return obj.get_total_points() + assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute" + return obj.total_points_attr + + def get_points(self, obj): + assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute" + if obj.role_points_attr is None: + return {} + + return obj.role_points_attr + + def get_comment(self, obj): + return "" + + def get_tasks(self, obj): + include_tasks = getattr(obj, "include_tasks", False) + + if include_tasks: + assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute" + + if not include_tasks or obj.tasks_attr is None: + return [] + + return obj.tasks_attr + + +class UserStorySerializer(UserStoryListSerializer): + comment = MethodField() + origin_issue = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() def get_comment(self, obj): # NOTE: This method and field is necessary to historical comments work return "" - def get_milestone_slug(self, obj): - if obj.milestone: - return obj.milestone.slug - else: - return None - - def get_milestone_name(self, obj): - if obj.milestone: - return obj.milestone.name - else: - return None - def get_origin_issue(self, obj): if obj.generated_from_issue: return { @@ -122,142 +137,5 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, return mdrender(obj.project, obj.description) -class ListOriginIssueSerializer(serializers.LightSerializer): - id = serpy.Field() - ref = serpy.Field() - subject = serpy.Field() - - def to_value(self, instance): - if instance is None: - return None - - return super().to_value(instance) - - -class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, - ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin, - serializers.LightSerializer): - - id = serpy.Field() - ref = serpy.Field() - milestone = serpy.Field(attr="milestone_id") - milestone_slug = serpy.MethodField() - milestone_name = serpy.MethodField() - project = serpy.Field(attr="project_id") - is_closed = serpy.Field() - points = serpy.MethodField() - backlog_order = serpy.Field() - sprint_order = serpy.Field() - kanban_order = serpy.Field() - created_date = serpy.Field() - modified_date = serpy.Field() - finish_date = serpy.Field() - subject = serpy.Field() - client_requirement = serpy.Field() - team_requirement = serpy.Field() - generated_from_issue = serpy.Field(attr="generated_from_issue_id") - external_reference = serpy.Field() - tribe_gig = serpy.Field() - version = serpy.Field() - watchers = serpy.Field() - is_blocked = serpy.Field() - blocked_note = serpy.Field() - tags = serpy.Field() - total_points = serpy.MethodField() - comment = serpy.MethodField("get_comment") - origin_issue = ListOriginIssueSerializer(attr="generated_from_issue") - - tasks = serpy.MethodField() - - def get_milestone_slug(self, obj): - return obj.milestone.slug if obj.milestone else None - - def get_milestone_name(self, obj): - return obj.milestone.name if obj.milestone else None - - def get_total_points(self, obj): - assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute" - return obj.total_points_attr - - def get_points(self, obj): - assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute" - if obj.role_points_attr is None: - return {} - - return dict(ChainMap(*obj.role_points_attr)) - - def get_comment(self, obj): - return "" - - def get_tasks(self, obj): - include_tasks = getattr(obj, "include_tasks", False) - - if include_tasks: - assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute" - - if not include_tasks or obj.tasks_attr is None: - return [] - - return obj.tasks_attr - - class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): - def serialize_neighbor(self, neighbor): - if neighbor: - return NeighborUserStorySerializer(neighbor).data - return None - - -class NeighborUserStorySerializer(serializers.ModelSerializer): - class Meta: - model = models.UserStory - fields = ("id", "ref", "subject") - depth = 0 - - -class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, - serializers.Serializer): - project_id = serializers.IntegerField() - status_id = serializers.IntegerField(required=False) - bulk_stories = serializers.CharField() - - -## Order bulk serializers - -class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serializer): - us_id = serializers.IntegerField() - order = serializers.IntegerField() - - -class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, - serializers.Serializer): - project_id = serializers.IntegerField() - bulk_stories = _UserStoryOrderBulkSerializer(many=True) - - -## Milestone bulk serializers - -class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer): - us_id = serializers.IntegerField() - - -class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - milestone_id = serializers.IntegerField() - bulk_stories = _UserStoryMilestoneBulkSerializer(many=True) - - def validate(self, data): - """ - All the userstories and the milestone are from the same project - """ - user_story_ids = [us["us_id"] for us in data["bulk_stories"]] - project = get_object_or_404(Project, pk=data["project_id"]) - - if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): - raise serializers.ValidationError("all the user stories must be from the same project") - - if project.milestones.filter(id=data["milestone_id"]).count() != 1: - raise serializers.ValidationError("the milestone isn't valid for the project") - - return data + pass diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index 809248f7..87b8e094 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -17,6 +17,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from taiga.projects.attachments.utils import attach_basic_attachments +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + def attach_total_points(queryset, as_field="total_points_attr"): """Attach total of point values to each object of the queryset. @@ -28,7 +35,7 @@ def attach_total_points(queryset, as_field="total_points_attr"): """ model = queryset.model sql = """SELECT SUM(projects_points.value) - FROM userstories_rolepoints + FROM userstories_rolepoints INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id WHERE userstories_rolepoints.user_story_id = {tbl}.id""" @@ -46,10 +53,15 @@ def attach_role_points(queryset, as_field="role_points_attr"): :return: Queryset object with the additional `as_field` field. """ model = queryset.model - sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id)) - FROM userstories_rolepoints + sql = """SELECT FORMAT('{{%%s}}', + STRING_AGG(format( + '"%%s":%%s', + TO_JSON(userstories_rolepoints.role_id), + TO_JSON(userstories_rolepoints.points_id) + ), ',') + )::json + FROM userstories_rolepoints WHERE userstories_rolepoints.user_story_id = {tbl}.id""" - sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset @@ -82,3 +94,23 @@ def attach_tasks(queryset, as_field="tasks_attr"): sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False): + queryset = attach_total_points(queryset) + queryset = attach_role_points(queryset) + + if include_attachments: + queryset = attach_basic_attachments(queryset) + queryset = queryset.extra(select={"include_attachments": "True"}) + + if include_tasks: + queryset = attach_tasks(queryset) + queryset = queryset.extra(select={"include_tasks": "True"}) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 5ad5e7f4..4ea0b24a 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -19,9 +19,21 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.api.utils import get_object_or_404 +from taiga.base.fields import PgArrayField +from taiga.base.fields import PickledObjectField +from taiga.projects.milestones.validators import MilestoneExistsValidator +from taiga.projects.models import Project +from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator from . import models +import json + class UserStoryExistsValidator: def validate_us_id(self, attrs, source): @@ -30,3 +42,72 @@ class UserStoryExistsValidator: msg = _("There's no user story with that id") raise serializers.ValidationError(msg) return attrs + + +class RolePointsField(serializers.WritableField): + def to_native(self, obj): + return {str(o.role.id): o.points.id for o in obj.all()} + + def from_native(self, obj): + if isinstance(obj, dict): + return obj + return json.loads(obj) + + +class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): + tags = TagsAndTagsColorsField(default=[], required=False) + external_reference = PgArrayField(required=False) + points = RolePointsField(source="role_points", required=False) + tribe_gig = PickledObjectField(required=False) + + class Meta: + model = models.UserStory + depth = 0 + read_only_fields = ('created_date', 'modified_date', 'owner') + + +class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator, + validators.Validator): + project_id = serializers.IntegerField() + status_id = serializers.IntegerField(required=False) + bulk_stories = serializers.CharField() + + +# Order bulk validators + +class _UserStoryOrderBulkValidator(UserStoryExistsValidator, validators.Validator): + us_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator, + validators.Validator): + project_id = serializers.IntegerField() + bulk_stories = _UserStoryOrderBulkValidator(many=True) + + +# Milestone bulk validators + +class _UserStoryMilestoneBulkValidator(UserStoryExistsValidator, validators.Validator): + us_id = serializers.IntegerField() + + +class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + milestone_id = serializers.IntegerField() + bulk_stories = _UserStoryMilestoneBulkValidator(many=True) + + def validate(self, data): + """ + All the userstories and the milestone are from the same project + """ + user_story_ids = [us["us_id"] for us in data["bulk_stories"]] + project = get_object_or_404(Project, pk=data["project_id"]) + + if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): + raise serializers.ValidationError("all the user stories must be from the same project") + + if project.milestones.filter(id=data["milestone_id"]).count() != 1: + raise serializers.ValidationError("the milestone isn't valid for the project") + + return data diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py index a05d8476..ee552136 100644 --- a/taiga/projects/utils.py +++ b/taiga/projects/utils.py @@ -375,7 +375,7 @@ def attach_private_projects_same_owner(queryset, user, as_field="private_project """ model = queryset.model if user is None or user.is_anonymous(): - sql = """SELECT '0'""" + sql = """SELECT 0""" else: sql = """SELECT COUNT(id) FROM projects_project p_aux @@ -399,7 +399,7 @@ def attach_public_projects_same_owner(queryset, user, as_field="public_projects_ """ model = queryset.model if user is None or user.is_anonymous(): - sql = """SELECT '0'""" + sql = """SELECT 0""" else: sql = """SELECT COUNT(id) FROM projects_project p_aux diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index 05866b66..c8ab21bb 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -16,11 +16,42 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.db.models import Q from django.utils.translation import ugettext as _ from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.fields import JsonField +from taiga.base.fields import PgArrayField +from taiga.users.validators import RoleExistsValidator + +from .tagging.fields import TagsField from . import models +from . import services + + +class DuplicatedNameInProjectValidator: + + def validate_name(self, attrs, source): + """ + Check the points name is not duplicated in the project on creation + """ + model = self.opts.model + qs = None + # If the object exists: + if self.object and attrs.get(source, None): + qs = model.objects.filter( + project=self.object.project, + name=attrs[source]).exclude(id=self.object.id) + + if not self.object and attrs.get("project", None) and attrs.get(source, None): + qs = model.objects.filter(project=attrs["project"], name=attrs[source]) + + if qs and qs.exists(): + raise serializers.ValidationError(_("Name duplicated for the project")) + + return attrs class ProjectExistsValidator: @@ -48,3 +79,170 @@ class TaskStatusExistsValidator: msg = _("There's no task status with that id") raise serializers.ValidationError(msg) return attrs + + +###################################################### +# Custom values for selectors +###################################################### + +class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.Points + + +class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.UserStoryStatus + + +class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.TaskStatus + + +class SeverityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.Severity + + +class PriorityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.Priority + + +class IssueStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.IssueStatus + + +class IssueTypeValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.IssueType + + +###################################################### +# Members +###################################################### + +class MembershipValidator(validators.ModelValidator): + email = serializers.EmailField(required=True) + + class Meta: + model = models.Membership + # IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date + # with this info (excluding here user_email and email) + read_only_fields = ("user",) + exclude = ("token", "email") + + def validate_email(self, attrs, source): + project = attrs.get("project", None) + if project is None: + project = self.object.project + + email = attrs[source] + + qs = models.Membership.objects.all() + + # If self.object is not None, the serializer is in update + # mode, and for it, it should exclude self. + if self.object: + qs = qs.exclude(pk=self.object.pk) + + qs = qs.filter(Q(project_id=project.id, user__email=email) | + Q(project_id=project.id, email=email)) + + if qs.count() > 0: + raise serializers.ValidationError(_("Email address is already taken")) + + return attrs + + def validate_role(self, attrs, source): + project = attrs.get("project", None) + if project is None: + project = self.object.project + + role = attrs[source] + + if project.roles.filter(id=role.id).count() == 0: + raise serializers.ValidationError(_("Invalid role for the project")) + + return attrs + + def validate_is_admin(self, attrs, source): + project = attrs.get("project", None) + if project is None: + project = self.object.project + + if (self.object and self.object.user): + if self.object.user.id == project.owner_id and not attrs[source]: + raise serializers.ValidationError(_("The project owner must be admin.")) + + if not services.project_has_valid_admins(project, exclude_user=self.object.user): + raise serializers.ValidationError( + _("At least one user must be an active admin for this project.") + ) + + return attrs + + +class MembershipAdminValidator(MembershipValidator): + class Meta: + model = models.Membership + # IMPORTANT: Maintain the MembershipSerializer Meta up to date + # with this info (excluding there user_email and email) + read_only_fields = ("user",) + exclude = ("token",) + + +class MemberBulkValidator(RoleExistsValidator, validators.Validator): + email = serializers.EmailField() + role_id = serializers.IntegerField() + + +class MembersBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_memberships = MemberBulkValidator(many=True) + invitation_extra_text = serializers.CharField(required=False, max_length=255) + + +###################################################### +# Projects +###################################################### + +class ProjectValidator(validators.ModelValidator): + anon_permissions = PgArrayField(required=False) + public_permissions = PgArrayField(required=False) + tags = TagsField(default=[], required=False) + + class Meta: + model = models.Project + read_only_fields = ("created_date", "modified_date", "slug", "blocked_code", "owner") + + +###################################################### +# Project Templates +###################################################### + +class ProjectTemplateValidator(validators.ModelValidator): + default_options = JsonField(required=False, label=_("Default options")) + us_statuses = JsonField(required=False, label=_("User story's statuses")) + points = JsonField(required=False, label=_("Points")) + task_statuses = JsonField(required=False, label=_("Task's statuses")) + issue_statuses = JsonField(required=False, label=_("Issue's statuses")) + issue_types = JsonField(required=False, label=_("Issue's types")) + priorities = JsonField(required=False, label=_("Priorities")) + severities = JsonField(required=False, label=_("Severities")) + roles = JsonField(required=False, label=_("Roles")) + + class Meta: + model = models.ProjectTemplate + read_only_fields = ("created_date", "modified_date") + + +###################################################### +# Project order bulk serializers +###################################################### + +class UpdateProjectOrderBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + order = serializers.IntegerField() diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py index fc7a988e..9f9d1049 100644 --- a/taiga/projects/votes/mixins/serializers.py +++ b/taiga/projects/votes/mixins/serializers.py @@ -16,12 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import serpy - from taiga.base.api import serializers +from taiga.base.fields import MethodField -class BaseVoteResourceSerializerMixin(object): +class VoteResourceSerializerMixin(serializers.LightSerializer): + is_voter = MethodField() + total_voters = MethodField() + def get_is_voter(self, obj): # The "is_voted" attribute is attached in the get_queryset of the viewset. return getattr(obj, "is_voter", False) or False @@ -29,13 +31,3 @@ class BaseVoteResourceSerializerMixin(object): def get_total_voters(self, obj): # The "total_voters" attribute is attached in the get_queryset of the viewset. return getattr(obj, "total_voters", 0) or 0 - - -class VoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serializers.ModelSerializer): - is_voter = serializers.SerializerMethodField("get_is_voter") - total_voters = serializers.SerializerMethodField("get_total_voters") - - -class ListVoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serpy.Serializer): - is_voter = serpy.MethodField("get_is_voter") - total_voters = serpy.MethodField("get_total_voters") diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py index 2456e375..50490ba7 100644 --- a/taiga/projects/votes/mixins/viewsets.py +++ b/taiga/projects/votes/mixins/viewsets.py @@ -39,14 +39,6 @@ class VotedResourceMixin: def pre_conditions_on_save(self, obj) """ - def attach_votes_attrs_to_queryset(self, queryset): - qs = attach_total_voters_to_queryset(queryset) - - if self.request.user.is_authenticated(): - qs = attach_is_voter_to_queryset(self.request.user, qs) - - return qs - @detail_route(methods=["POST"]) def upvote(self, request, pk=None): obj = self.get_object() diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py index 291ee284..077abd46 100644 --- a/taiga/projects/votes/utils.py +++ b/taiga/projects/votes/utils.py @@ -48,7 +48,7 @@ def attach_total_voters_to_queryset(queryset, as_field="total_voters"): return qs -def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"): +def attach_is_voter_to_queryset(queryset, user, as_field="is_voter"): """Attach is_vote boolean to each object of the queryset. Because of laziness of vote objects creation, this makes much simpler and more efficient to @@ -57,22 +57,26 @@ def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"): (The other way was to do it in the serializer with some try/except blocks and additional queries) - :param user: A users.User object model :param queryset: A Django queryset object. + :param user: A users.User object model :param as_field: Attach the boolean as an attribute with this name. :return: Queryset object with the additional `as_field` field. """ model = queryset.model type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) - sql = ("""SELECT CASE WHEN (SELECT count(*) - FROM votes_vote - WHERE votes_vote.content_type_id = {type_id} - AND votes_vote.object_id = {tbl}.id - AND votes_vote.user_id = {user_id}) > 0 - THEN TRUE - ELSE FALSE - END""") - sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + if user is None or user.is_anonymous(): + sql = """SELECT false""" + else: + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM votes_vote + WHERE votes_vote.content_type_id = {type_id} + AND votes_vote.object_id = {tbl}.id + AND votes_vote.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + qs = queryset.extra(select={as_field: sql}) return qs diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index 807d86c6..d6a3d44a 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -24,7 +24,6 @@ from taiga.base import response from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.base.api.permissions import IsAuthenticated from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route @@ -42,6 +41,8 @@ from taiga.projects.occ import OCCResourceMixin from . import models from . import permissions from . import serializers +from . import validators +from . import utils as wiki_utils class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, @@ -49,6 +50,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, model = models.WikiPage serializer_class = serializers.WikiPageSerializer + validator_class = validators.WikiPageValidator permission_classes = (permissions.WikiPagePermission,) filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ("project", "slug") @@ -56,7 +58,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, def get_queryset(self): qs = super().get_queryset() - qs = self.attach_watchers_attrs_to_queryset(qs) + qs = wiki_utils.attach_extra_info(qs, user=self.request.user) return qs @list_route(methods=["GET"]) @@ -100,6 +102,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.WikiLink serializer_class = serializers.WikiLinkSerializer + validator_class = validators.WikiLinkValidator permission_classes = (permissions.WikiLinkPermission,) filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ["project"] @@ -120,7 +123,7 @@ class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): wiki_page, created = models.WikiPage.objects.get_or_create( slug=wiki_link.href, project=wiki_link.project, - defaults={"owner": self.request.user,"last_modifier": self.request.user}) + defaults={"owner": self.request.user, "last_modifier": self.request.user}) if created: # Creaste the new history entre, sSet watcher for the new wiki page diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py index 16de19df..a7e36c60 100644 --- a/taiga/projects/wiki/serializers.py +++ b/taiga/projects/wiki/serializers.py @@ -17,21 +17,26 @@ # along with this program. If not, see . from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField from taiga.projects.history import services as history_service -from taiga.projects.notifications.mixins import WatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.mdrender.service import render as mdrender -from . import models +class WikiPageSerializer(WatchedResourceSerializer, serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + slug = Field() + content = Field() + owner = Field(attr="owner_id") + last_modifier = Field(attr="last_modifier_id") + created_date = Field() + modified_date = Field() -class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer): - html = serializers.SerializerMethodField("get_html") - editions = serializers.SerializerMethodField("get_editions") + html = MethodField() + editions = MethodField() - class Meta: - model = models.WikiPage - read_only_fields = ('modified_date', 'created_date', 'owner') + version = Field() def get_html(self, obj): return mdrender(obj.project, obj.content) @@ -40,7 +45,9 @@ class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, seri return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation -class WikiLinkSerializer(serializers.ModelSerializer): - class Meta: - model = models.WikiLink - read_only_fields = ('href',) +class WikiLinkSerializer(serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + title = Field() + href = Field() + order = Field() diff --git a/taiga/projects/wiki/utils.py b/taiga/projects/wiki/utils.py new file mode 100644 index 00000000..ecbf7602 --- /dev/null +++ b/taiga/projects/wiki/utils.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False): + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/wiki/validators.py b/taiga/projects/wiki/validators.py new file mode 100644 index 00000000..033fac1b --- /dev/null +++ b/taiga/projects/wiki/validators.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import validators +from taiga.projects.notifications.validators import WatchersValidator + +from . import models + + +class WikiPageValidator(WatchersValidator, validators.ModelValidator): + class Meta: + model = models.WikiPage + read_only_fields = ('modified_date', 'created_date', 'owner') + + +class WikiLinkValidator(validators.ModelValidator): + class Meta: + model = models.WikiLink + read_only_fields = ('href',) diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py index edc2d1ca..e96e1131 100644 --- a/taiga/searches/serializers.py +++ b/taiga/searches/serializers.py @@ -16,37 +16,48 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.projects.issues.serializers import IssueSerializer -from taiga.projects.userstories.serializers import UserStorySerializer -from taiga.projects.tasks.serializers import TaskSerializer -from taiga.projects.wiki.serializers import WikiPageSerializer - -from taiga.projects.issues.models import Issue -from taiga.projects.userstories.models import UserStory -from taiga.projects.tasks.models import Task -from taiga.projects.wiki.models import WikiPage +from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField -class IssueSearchResultsSerializer(IssueSerializer): - class Meta: - model = Issue - fields = ('id', 'ref', 'subject', 'status', 'assigned_to') +class IssueSearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + assigned_to = Field(attr="assigned_to_id") -class TaskSearchResultsSerializer(TaskSerializer): - class Meta: - model = Task - fields = ('id', 'ref', 'subject', 'status', 'assigned_to') +class TaskSearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + assigned_to = Field(attr="assigned_to_id") -class UserStorySearchResultsSerializer(UserStorySerializer): - class Meta: - model = UserStory - fields = ('id', 'ref', 'subject', 'status', 'total_points', - 'milestone_name', 'milestone_slug') +class UserStorySearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + total_points = MethodField() + milestone_name = MethodField() + milestone_slug = MethodField() + + def get_milestone_name(self, obj): + return obj.milestone.name if obj.milestone else None + + def get_milestone_slug(self, obj): + return obj.milestone.slug if obj.milestone else None + + def get_total_points(self, obj): + assert hasattr(obj, "total_points_attr"), \ + "instance must have a total_points_attr attribute" + + return obj.total_points_attr -class WikiPageSearchResultsSerializer(WikiPageSerializer): - class Meta: - model = WikiPage - fields = ('id', 'slug') +class WikiPageSearchResultsSerializer(serializers.LightSerializer): + id = Field() + slug = Field() diff --git a/taiga/searches/services.py b/taiga/searches/services.py index f393844f..4dcda86f 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -19,6 +19,7 @@ from django.apps import apps from django.conf import settings from taiga.base.utils.db import to_tsquery +from taiga.projects.userstories.utils import attach_total_points MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) @@ -30,11 +31,13 @@ def search_user_stories(project, text): "coalesce(userstories_userstory.description, '')) " "@@ to_tsquery('english_nostop', %s)") - if text: - return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) - .filter(project_id=project.pk)[:MAX_RESULTS]) + queryset = model_cls.objects.filter(project_id=project.pk) - return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] + if text: + queryset = queryset.extra(where=[where_clause], params=[to_tsquery(text)]) + + queryset = attach_total_points(queryset) + return queryset[:MAX_RESULTS] def search_tasks(project, text): diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index b0bf8e13..3e3bd6f4 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -18,10 +18,8 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from django.apps import apps from taiga.base import response -from taiga.base.api.utils import get_object_or_404 from taiga.base.api import ReadOnlyListViewSet from . import serializers @@ -36,7 +34,7 @@ class TimelineViewSet(ReadOnlyListViewSet): def get_content_type(self): app_name, model = self.content_type.split(".", 1) - return get_object_or_404(ContentType, app_label=app_name, model=model) + return ContentType.objects.get_by_natural_key(app_name, model) def get_queryset(self): ct = self.get_content_type() diff --git a/taiga/timeline/migrations/0005_auto_20160706_0723.py b/taiga/timeline/migrations/0005_auto_20160706_0723.py new file mode 100644 index 00000000..7ac9fa9c --- /dev/null +++ b/taiga/timeline/migrations/0005_auto_20160706_0723.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-06 07:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('timeline', '0004_auto_20150603_1312'), + ] + + operations = [ + migrations.AlterField( + model_name='timeline', + name='created', + field=models.DateTimeField(db_index=True, default=django.utils.timezone.now), + ), + ] diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index c71188f7..ebee7da5 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -20,13 +20,12 @@ from django.db import models from django_pgjson.fields import JsonField from django.utils import timezone -from django.core.exceptions import ValidationError - from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from taiga.projects.models import Project + class Timeline(models.Model): content_type = models.ForeignKey(ContentType, related_name="content_type_timelines") object_id = models.PositiveIntegerField() @@ -36,12 +35,11 @@ class Timeline(models.Model): project = models.ForeignKey(Project, null=True) data = JsonField() data_content_type = models.ForeignKey(ContentType, related_name="data_timelines") - created = models.DateTimeField(default=timezone.now) + created = models.DateTimeField(default=timezone.now, db_index=True) class Meta: index_together = [('content_type', 'object_id', 'namespace'), ] - # Register all implementations from .timeline_implementations import * diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index a6be6944..07b1985a 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -16,26 +16,32 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.apps import apps from django.contrib.auth import get_user_model -from django.forms import widgets from taiga.base.api import serializers -from taiga.base.fields import JsonField +from taiga.base.fields import Field, MethodField from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url from . import models -from . import service -class TimelineSerializer(serializers.ModelSerializer): +class TimelineSerializer(serializers.LightSerializer): data = serializers.SerializerMethodField("get_data") + id = Field() + content_type = Field(attr="content_type_id") + object_id = Field() + namespace = Field() + event_type = Field() + project = Field(attr="project_id") + data = MethodField() + data_content_type = Field(attr="data_content_type_id") + created = Field() class Meta: model = models.Timeline def get_data(self, obj): - #Updates the data user info saved if the user exists + # Updates the data user info saved if the user exists if hasattr(obj, "_prefetched_user"): user = obj._prefetched_user else: diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index f99f795e..d3e81976 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -27,33 +27,32 @@ from functools import partial, wraps from taiga.base.utils.db import get_typename_for_model_class from taiga.celery import app -from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url _timeline_impl_map = {} -def _get_impl_key_from_model(model:Model, event_type:str): +def _get_impl_key_from_model(model: Model, event_type: str): if issubclass(model, Model): typename = get_typename_for_model_class(model) return _get_impl_key_from_typename(typename, event_type) raise Exception("Not valid model parameter") -def _get_impl_key_from_typename(typename:str, event_type:str): +def _get_impl_key_from_typename(typename: str, event_type: str): if isinstance(typename, str): return "{0}.{1}".format(typename, event_type) raise Exception("Not valid typename parameter") -def build_user_namespace(user:object): +def build_user_namespace(user: object): return "{0}:{1}".format("user", user.id) -def build_project_namespace(project:object): +def build_project_namespace(project: object): return "{0}:{1}".format("project", project.id) -def _add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model" from .models import Timeline @@ -75,12 +74,12 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, created ) -def _add_to_objects_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): for obj in objects: _add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data) -def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): if isinstance(objects, Model): _add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data) elif isinstance(objects, QuerySet) or isinstance(objects, list): @@ -111,10 +110,10 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id except projectModel.DoesNotExist: return - ## Project timeline + # Project timeline _push_to_timeline(project, obj, event_type, created_datetime, - namespace=build_project_namespace(project), - extra_data=extra_data) + namespace=build_project_namespace(project), + extra_data=extra_data) project.refresh_totals() @@ -122,14 +121,14 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id related_people = obj.get_related_people() _push_to_timeline(related_people, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + namespace=build_user_namespace(user), + extra_data=extra_data) else: # Actions not related with a project - ## - Me + # - Me _push_to_timeline(user, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + namespace=build_user_namespace(user), + extra_data=extra_data) def get_timeline(obj, namespace=None): @@ -141,7 +140,6 @@ def get_timeline(obj, namespace=None): if namespace is not None: timeline = timeline.filter(namespace=namespace) - timeline = timeline.select_related("project") timeline = timeline.order_by("-created", "-id") return timeline @@ -156,22 +154,22 @@ def filter_timeline_for_user(timeline, user): # Filtering private project with some public parts content_types = { - "view_project": ContentType.objects.get(app_label="projects", model="project"), - "view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"), - "view_us": ContentType.objects.get(app_label="userstories", model="userstory"), - "view_tasks": ContentType.objects.get(app_label="tasks", model="task"), - "view_issues": ContentType.objects.get(app_label="issues", model="issue"), - "view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"), - "view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"), + "view_project": ContentType.objects.get_by_natural_key("projects", "project"), + "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"), + "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"), + "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"), + "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"), + "view_wiki_pages": ContentType.objects.get_by_natural_key("wiki", "wikipage"), + "view_wiki_links": ContentType.objects.get_by_natural_key("wiki", "wikilink"), } for content_type_key, content_type in content_types.items(): tl_filter |= Q(project__is_private=True, - project__anon_permissions__contains=[content_type_key], - data_content_type=content_type) + project__anon_permissions__contains=[content_type_key], + data_content_type=content_type) # There is no specific permission for seeing new memberships - membership_content_type = ContentType.objects.get(app_label="projects", model="membership") + membership_content_type = ContentType.objects.get_by_natural_key(app_label="projects", model="membership") tl_filter |= Q(project__is_private=True, project__anon_permissions__contains=["view_project"], data_content_type=membership_content_type) @@ -214,7 +212,7 @@ def get_project_timeline(project, accessing_user=None): return timeline -def register_timeline_implementation(typename:str, event_type:str, fn=None): +def register_timeline_implementation(typename: str, event_type: str, fn=None): assert isinstance(typename, str), "typename must be a string" assert isinstance(event_type, str), "event_type must be a string" @@ -231,7 +229,6 @@ def register_timeline_implementation(typename:str, event_type:str, fn=None): return _wrapper - def extract_project_info(instance): return { "id": instance.pk, diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 98903ec1..3d584c53 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -22,7 +22,8 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers -from taiga.base.fields import PgArrayField +from taiga.base.fields import PgArrayField, Field, MethodField + from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project @@ -33,11 +34,10 @@ from .gravatar import get_gravatar_url from collections import namedtuple import re -import serpy ###################################################### -## User +# User ###################################################### class ContactProjectDetailSerializer(serializers.ModelSerializer): @@ -139,19 +139,13 @@ class UserAdminSerializer(UserSerializer): return user.owned_projects.filter(is_private=False).count() -class UserBasicInfoSerializer(UserSerializer): - class Meta: - model = User - fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id") - - -class ListUserBasicInfoSerializer(serpy.Serializer): - username = serpy.Field() - full_name_display = serpy.MethodField() - photo = serpy.MethodField() - big_photo = serpy.MethodField() - is_active = serpy.Field() - id = serpy.Field() +class UserBasicInfoSerializer(serializers.LightSerializer): + username = Field() + full_name_display = MethodField() + photo = MethodField() + big_photo = MethodField() + is_active = Field() + id = Field() def get_full_name_display(self, obj): return obj.get_full_name() @@ -162,6 +156,12 @@ class ListUserBasicInfoSerializer(serpy.Serializer): def get_big_photo(self, obj): return get_big_photo_or_gravatar_url(obj) + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) + class RecoverySerializer(serializers.Serializer): token = serializers.CharField(max_length=200) @@ -177,7 +177,7 @@ class CancelAccountSerializer(serializers.Serializer): ###################################################### -## Role +# Role ###################################################### class RoleSerializer(serializers.ModelSerializer): @@ -201,7 +201,7 @@ class ProjectRoleSerializer(serializers.ModelSerializer): ###################################################### -## Like +# Like ###################################################### class HighLightedContentSerializer(serializers.Serializer): diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index f15021a0..4648a73a 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -30,6 +30,7 @@ from taiga.base.decorators import detail_route from . import models from . import serializers +from . import validators from . import permissions from . import tasks @@ -37,6 +38,7 @@ from . import tasks class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Webhook serializer_class = serializers.WebhookSerializer + validator_class = validators.WebhookValidator permission_classes = (permissions.WebhookPermission,) filter_backends = (filters.IsProjectAdminFilterBackend,) filter_fields = ("project",) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index ee0d8308..624e107c 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -19,63 +19,55 @@ from django.core.exceptions import ObjectDoesNotExist from taiga.base.api import serializers -from taiga.base.fields import PgArrayField, JsonField - +from taiga.base.fields import Field, MethodField from taiga.front.templatetags.functions import resolve as resolve_front_url -from taiga.projects.history import models as history_models -from taiga.projects.issues import models as issue_models -from taiga.projects.milestones import models as milestone_models -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.services import get_logo_big_thumbnail_url -from taiga.projects.tasks import models as task_models -from taiga.projects.tagging.fields import TagsField -from taiga.projects.userstories import models as us_models -from taiga.projects.wiki import models as wiki_models from taiga.users.gravatar import get_gravatar_url from taiga.users.services import get_photo_or_gravatar_url -from .models import Webhook, WebhookLog - ######################################################################## -## WebHooks +# WebHooks ######################################################################## -class WebhookSerializer(serializers.ModelSerializer): - logs_counter = serializers.SerializerMethodField("get_logs_counter") - - class Meta: - model = Webhook +class WebhookSerializer(serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + name = Field() + url = Field() + key = Field() + logs_counter = MethodField() def get_logs_counter(self, obj): return obj.logs.count() -class WebhookLogSerializer(serializers.ModelSerializer): - request_data = JsonField() - request_headers = JsonField() - response_headers = JsonField() - - class Meta: - model = WebhookLog +class WebhookLogSerializer(serializers.LightSerializer): + id = Field() + webhook = Field(attr="webhook_id") + url = Field() + status = Field() + request_data = Field() + request_headers = Field() + response_data = Field() + response_headers = Field() + duration = Field() + created = Field() ######################################################################## -## User +# User ######################################################################## -class UserSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - permalink = serializers.SerializerMethodField("get_permalink") - gravatar_url = serializers.SerializerMethodField("get_gravatar_url") - username = serializers.SerializerMethodField("get_username") - full_name = serializers.SerializerMethodField("get_full_name") - photo = serializers.SerializerMethodField("get_photo") - - def get_pk(self, obj): - return obj.pk +class UserSerializer(serializers.LightSerializer): + id = Field(attr="pk") + permalink = MethodField() + gravatar_url = MethodField() + username = MethodField() + full_name = MethodField() + photo = MethodField() def get_permalink(self, obj): return resolve_front_url("user", obj.username) @@ -84,7 +76,7 @@ class UserSerializer(serializers.Serializer): return get_gravatar_url(obj.email) def get_username(self, obj): - return obj.get_username + return obj.get_username() def get_full_name(self, obj): return obj.get_full_name() @@ -92,18 +84,22 @@ class UserSerializer(serializers.Serializer): def get_photo(self, obj): return get_photo_or_gravatar_url(obj) + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) + + ######################################################################## -## Project +# Project ######################################################################## -class ProjectSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - permalink = serializers.SerializerMethodField("get_permalink") - name = serializers.SerializerMethodField("get_name") - logo_big_url = serializers.SerializerMethodField("get_logo_big_url") - - def get_pk(self, obj): - return obj.pk +class ProjectSerializer(serializers.LightSerializer): + id = Field(attr="pk") + permalink = MethodField() + name = MethodField() + logo_big_url = MethodField() def get_permalink(self, obj): return resolve_front_url("project", obj.slug) @@ -116,11 +112,11 @@ class ProjectSerializer(serializers.Serializer): ######################################################################## -## History Serializer +# History Serializer ######################################################################## -class HistoryDiffField(serializers.Field): - def to_native(self, value): +class HistoryDiffField(Field): + def to_value(self, value): # Tip: 'value' is the object returned by # taiga.projects.history.models.HistoryEntry.values_diff() @@ -137,21 +133,21 @@ class HistoryDiffField(serializers.Field): return ret -class HistoryEntrySerializer(serializers.ModelSerializer): - diff = HistoryDiffField(source="values_diff") - - class Meta: - model = history_models.HistoryEntry - exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user", - "values", "created_at") +class HistoryEntrySerializer(serializers.LightSerializer): + comment = Field() + comment_html = Field() + delete_comment_date = Field() + comment_versions = Field() + edit_comment_date = Field() + diff = HistoryDiffField(attr="values_diff") ######################################################################## -## _Misc_ +# _Misc_ ######################################################################## -class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): - custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") +class CustomAttributesValuesWebhookSerializerMixin(serializers.LightSerializer): + custom_attributes_values = MethodField() def custom_attributes_queryset(self, project): raise NotImplementedError() @@ -161,13 +157,13 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): ret = {} for attr in custom_attributes: value = values.get(str(attr["id"]), None) - if value is not None: + if value is not None: ret[attr["name"]] = value return ret try: - values = obj.custom_attributes_values.attributes_values + values = obj.custom_attributes_values.attributes_values custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name') return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) @@ -175,10 +171,10 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): return None -class RolePointsSerializer(serializers.Serializer): - role = serializers.SerializerMethodField("get_role") - name = serializers.SerializerMethodField("get_name") - value = serializers.SerializerMethodField("get_value") +class RolePointsSerializer(serializers.LightSerializer): + role = MethodField() + name = MethodField() + value = MethodField() def get_role(self, obj): return obj.role.name @@ -190,16 +186,13 @@ class RolePointsSerializer(serializers.Serializer): return obj.points.value -class UserStoryStatusSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - slug = serializers.SerializerMethodField("get_slug") - color = serializers.SerializerMethodField("get_color") - is_closed = serializers.SerializerMethodField("get_is_closed") - is_archived = serializers.SerializerMethodField("get_is_archived") - - def get_pk(self, obj): - return obj.pk +class UserStoryStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() + is_archived = MethodField() def get_name(self, obj): return obj.name @@ -217,15 +210,12 @@ class UserStoryStatusSerializer(serializers.Serializer): return obj.is_archived -class TaskStatusSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - slug = serializers.SerializerMethodField("get_slug") - color = serializers.SerializerMethodField("get_color") - is_closed = serializers.SerializerMethodField("get_is_closed") - - def get_pk(self, obj): - return obj.pk +class TaskStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() def get_name(self, obj): return obj.name @@ -240,15 +230,12 @@ class TaskStatusSerializer(serializers.Serializer): return obj.is_closed -class IssueStatusSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - slug = serializers.SerializerMethodField("get_slug") - color = serializers.SerializerMethodField("get_color") - is_closed = serializers.SerializerMethodField("get_is_closed") - - def get_pk(self, obj): - return obj.pk +class IssueStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() def get_name(self, obj): return obj.name @@ -263,13 +250,10 @@ class IssueStatusSerializer(serializers.Serializer): return obj.is_closed -class IssueTypeSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - color = serializers.SerializerMethodField("get_color") - - def get_pk(self, obj): - return obj.pk +class IssueTypeSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + color = MethodField() def get_name(self, obj): return obj.name @@ -278,13 +262,10 @@ class IssueTypeSerializer(serializers.Serializer): return obj.color -class PrioritySerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - color = serializers.SerializerMethodField("get_color") - - def get_pk(self, obj): - return obj.pk +class PrioritySerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + color = MethodField() def get_name(self, obj): return obj.name @@ -293,13 +274,10 @@ class PrioritySerializer(serializers.Serializer): return obj.color -class SeveritySerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - color = serializers.SerializerMethodField("get_color") - - def get_pk(self, obj): - return obj.pk +class SeveritySerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + color = MethodField() def get_name(self, obj): return obj.name @@ -309,57 +287,90 @@ class SeveritySerializer(serializers.Serializer): ######################################################################## -## Milestone +# Milestone ######################################################################## -class MilestoneSerializer(serializers.ModelSerializer): +class MilestoneSerializer(serializers.LightSerializer): + id = Field() + name = Field() + slug = Field() + estimated_start = Field() + estimated_finish = Field() + created_date = Field() + modified_date = Field() + closed = Field() + disponibility = Field() permalink = serializers.SerializerMethodField("get_permalink") project = ProjectSerializer() owner = UserSerializer() - class Meta: - model = milestone_models.Milestone - exclude = ("order", "watchers") - def get_permalink(self, obj): return resolve_front_url("taskboard", obj.project.slug, obj.slug) ######################################################################## -## User Story +# User Story ######################################################################## -class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - permalink = serializers.SerializerMethodField("get_permalink") - tags = TagsField(default=[], required=False) - external_reference = PgArrayField(required=False) +class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() project = ProjectSerializer() + is_closed = Field() + created_date = Field() + modified_date = Field() + finish_date = Field() + subject = Field() + client_requirement = Field() + team_requirement = Field() + generated_from_issue = Field(attr="generated_from_issue_id") + external_reference = Field() + tribe_gig = Field() + watchers = MethodField() + is_blocked = Field() + blocked_note = Field() + tags = Field() + permalink = serializers.SerializerMethodField("get_permalink") owner = UserSerializer() assigned_to = UserSerializer() - points = RolePointsSerializer(source="role_points", many=True) + points = MethodField() status = UserStoryStatusSerializer() milestone = MilestoneSerializer() - class Meta: - model = us_models.UserStory - exclude = ("backlog_order", "sprint_order", "kanban_order", "version", "total_watchers", "is_watcher") - def get_permalink(self, obj): return resolve_front_url("userstory", obj.project.slug, obj.ref) def custom_attributes_queryset(self, project): return project.userstorycustomattributes.all() + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + + def get_points(self, obj): + return RolePointsSerializer(obj.role_points.all(), many=True).data + ######################################################################## -## Task +# Task ######################################################################## -class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): +class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + us_order = Field() + taskboard_order = Field() + is_iocaine = Field() + external_reference = Field() + watchers = MethodField() + is_blocked = Field() + blocked_note = Field() + description = Field() + tags = Field() permalink = serializers.SerializerMethodField("get_permalink") - tags = TagsField(default=[], required=False) project = ProjectSerializer() owner = UserSerializer() assigned_to = UserSerializer() @@ -367,25 +378,32 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatch user_story = UserStorySerializer() milestone = MilestoneSerializer() - class Meta: - model = task_models.Task - exclude = ("version", "total_watchers", "is_watcher") - def get_permalink(self, obj): return resolve_front_url("task", obj.project.slug, obj.ref) def custom_attributes_queryset(self, project): return project.taskcustomattributes.all() + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + ######################################################################## -## Issue +# Issue ######################################################################## -class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): +class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + external_reference = Field() + watchers = MethodField() + description = Field() + tags = Field() permalink = serializers.SerializerMethodField("get_permalink") - tags = TagsField(default=[], required=False) project = ProjectSerializer() milestone = MilestoneSerializer() owner = UserSerializer() @@ -395,30 +413,30 @@ class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatc priority = PrioritySerializer() severity = SeveritySerializer() - class Meta: - model = issue_models.Issue - exclude = ("version", "total_watchers", "is_watcher") - def get_permalink(self, obj): return resolve_front_url("issue", obj.project.slug, obj.ref) def custom_attributes_queryset(self, project): return project.issuecustomattributes.all() + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + ######################################################################## -## Wiki Page +# Wiki Page ######################################################################## -class WikiPageSerializer(serializers.ModelSerializer): +class WikiPageSerializer(serializers.LightSerializer): + id = Field() + slug = Field() + content = Field() + created_date = Field() + modified_date = Field() permalink = serializers.SerializerMethodField("get_permalink") project = ProjectSerializer() owner = UserSerializer() last_modifier = UserSerializer() - class Meta: - model = wiki_models.WikiPage - exclude = ("watchers", "total_watchers", "is_watcher", "version") - def get_permalink(self, obj): return resolve_front_url("wiki", obj.project.slug, obj.slug) diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index 7990b928..334cd52d 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -149,5 +149,4 @@ def test_webhook(webhook_id, url, key, by, date): data['by'] = UserSerializer(by).data data['date'] = date data['data'] = {"test": "test"} - return _send_request(webhook_id, url, key, data) diff --git a/taiga/webhooks/validators.py b/taiga/webhooks/validators.py new file mode 100644 index 00000000..b95e2e64 --- /dev/null +++ b/taiga/webhooks/validators.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import validators + +from .models import Webhook + + +class WebhookValidator(validators.ModelValidator): + class Meta: + model = Webhook diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index d9b391f8..4f93ea0c 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -22,7 +22,11 @@ import uuid from django.core.urlresolvers import reverse from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.projects.issues.models import Issue from taiga.projects.issues.serializers import IssueSerializer +from taiga.projects.issues.utils import attach_extra_info as attach_issue_extra_info from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json @@ -61,22 +65,29 @@ def data(): public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -129,24 +140,31 @@ def data(): priority__project=m.public_project, type__project=m.public_project, milestone__project=m.public_project) + m.public_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.public_issue.id) + m.private_issue1 = f.IssueFactory(project=m.private_project1, status__project=m.private_project1, severity__project=m.private_project1, priority__project=m.private_project1, type__project=m.private_project1, milestone__project=m.private_project1) + m.private_issue1 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue1.id) + m.private_issue2 = f.IssueFactory(project=m.private_project2, status__project=m.private_project2, severity__project=m.private_project2, priority__project=m.private_project2, type__project=m.private_project2, milestone__project=m.private_project2) + m.private_issue2 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue2.id) + m.blocked_issue = f.IssueFactory(project=m.blocked_project, status__project=m.blocked_project, severity__project=m.blocked_project, priority__project=m.blocked_project, type__project=m.blocked_project, milestone__project=m.blocked_project) + m.blocked_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.blocked_issue.id) return m @@ -443,24 +461,28 @@ def test_issue_put_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) issue = f.IssueFactory.create(project=project1) + issue = attach_issue_extra_info(Issue.objects.all()).get(id=issue.id) url = reverse('issues-detail', kwargs={"pk": issue.pk}) diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index a1a06172..1a3ab5cb 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -22,8 +22,11 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone +from taiga.projects.milestones.utils import attach_extra_info as attach_milestone_extra_info from taiga.projects.notifications.services import add_watcher from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS @@ -56,44 +59,55 @@ def data(): anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, role__project=m.private_project1, role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project2, user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -112,13 +126,17 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) m.public_milestone = f.MilestoneFactory(project=m.public_project) + m.public_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.public_milestone.id) m.private_milestone1 = f.MilestoneFactory(project=m.private_project1) + m.private_milestone1 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone1.id) m.private_milestone2 = f.MilestoneFactory(project=m.private_project2) + m.private_milestone2 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone2.id) m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project) + m.blocked_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.blocked_milestone.id) return m @@ -422,16 +440,16 @@ def test_milestone_watchers_list(client, data): def test_milestone_watchers_retrieve(client, data): add_watcher(data.public_milestone, data.project_owner) public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_milestone1, data.project_owner) private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_milestone2, data.project_owner) private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_milestone, data.project_owner) blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 949893b6..1410c86d 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -22,8 +22,10 @@ from django.apps import apps from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects import models as project_models from taiga.projects.serializers import ProjectSerializer from taiga.permissions.choices import MEMBERS_PERMISSIONS +from taiga.projects.utils import attach_extra_info from tests import factories as f from tests.utils import helper_test_http_method, helper_test_http_method_and_count @@ -45,19 +47,26 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=['view_project'], public_permissions=['view_project']) + m.public_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=['view_project'], public_permissions=['view_project'], owner=m.project_owner) + m.private_project1 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.private_project2 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.blocked_project.id) f.RoleFactory(project=m.public_project) diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 5eaf5243..4d6427dd 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -23,12 +23,16 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project from taiga.projects.tasks.serializers import TaskSerializer +from taiga.projects.tasks.models import Task +from taiga.projects.tasks.utils import attach_extra_info as attach_task_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f -from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from tests.utils import helper_test_http_method, reconnect_signals from taiga.projects.votes.services import add_vote from taiga.projects.notifications.services import add_watcher @@ -38,10 +42,6 @@ import pytest pytestmark = pytest.mark.django_db -def setup_function(function): - disconnect_signals() - - def setup_function(function): reconnect_signals() @@ -61,47 +61,61 @@ def data(): public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project1, - user=m.project_member_without_perms, - role__project=m.private_project1, - role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project2, - user=m.project_member_without_perms, - role__project=m.private_project2, - role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -120,8 +134,8 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) milestone_public_task = f.MilestoneFactory(project=m.public_project) milestone_private_task1 = f.MilestoneFactory(project=m.private_project1) @@ -133,21 +147,28 @@ def data(): milestone=milestone_public_task, user_story__project=m.public_project, user_story__milestone=milestone_public_task) + m.public_task = attach_task_extra_info(Task.objects.all()).get(id=m.public_task.id) + m.private_task1 = f.TaskFactory(project=m.private_project1, status__project=m.private_project1, milestone=milestone_private_task1, user_story__project=m.private_project1, user_story__milestone=milestone_private_task1) + m.private_task1 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task1.id) + m.private_task2 = f.TaskFactory(project=m.private_project2, status__project=m.private_project2, milestone=milestone_private_task2, user_story__project=m.private_project2, user_story__milestone=milestone_private_task2) + m.private_task2 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task2.id) + m.blocked_task = f.TaskFactory(project=m.blocked_project, - status__project=m.blocked_project, - milestone=milestone_blocked_task, - user_story__project=m.blocked_project, - user_story__milestone=milestone_blocked_task) + status__project=m.blocked_project, + milestone=milestone_blocked_task, + user_story__project=m.blocked_project, + user_story__milestone=milestone_blocked_task) + m.blocked_task = attach_task_extra_info(Task.objects.all()).get(id=m.blocked_task.id) m.public_project.default_task_status = m.public_task.status m.public_project.save() @@ -404,24 +425,28 @@ def test_task_put_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) task = f.TaskFactory.create(project=project1) + task = attach_task_extra_info(Task.objects.all()).get(id=task.id) url = reverse('tasks-detail', kwargs={"pk": task.pk}) @@ -739,17 +764,17 @@ def test_task_voters_list(client, data): def test_task_voters_retrieve(client, data): add_vote(data.public_task, data.project_owner) public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.private_task1, data.project_owner) private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.private_task2, data.project_owner) private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.blocked_task, data.project_owner) blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, @@ -844,17 +869,17 @@ def test_task_watchers_list(client, data): def test_task_watchers_retrieve(client, data): add_watcher(data.public_task, data.project_owner) public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_task1, data.project_owner) private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_task2, data.project_owner) private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_task, data.project_owner) blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, data.registered_user, diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index c9f95a31..4eb0c416 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -23,7 +23,11 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.projects.userstories.models import UserStory from taiga.projects.userstories.serializers import UserStorySerializer +from taiga.projects.userstories.utils import attach_extra_info as attach_userstory_extra_info from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin @@ -61,47 +65,58 @@ def data(): public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) - m.blocked_project = f.ProjectFactory(is_private=True, - anon_permissions=[], - public_permissions=[], - owner=m.project_owner, - userstories_csv_uuid=uuid.uuid4().hex, - blocked_code=project_choices.BLOCKED_BY_STAFF) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, role__project=m.private_project1, role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project2, user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -120,8 +135,8 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) @@ -144,15 +159,19 @@ def data(): user_story__milestone__project=m.private_project2, user_story__status__project=m.private_project2) m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0], - points=m.blocked_points, - user_story__project=m.blocked_project, - user_story__milestone__project=m.blocked_project, - user_story__status__project=m.blocked_project) + points=m.blocked_points, + user_story__project=m.blocked_project, + user_story__milestone__project=m.blocked_project, + user_story__status__project=m.blocked_project) m.public_user_story = m.public_role_points.user_story + m.public_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.public_user_story.id) m.private_user_story1 = m.private_role_points1.user_story + m.private_user_story1 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story1.id) m.private_user_story2 = m.private_role_points2.user_story + m.private_user_story2 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story2.id) m.blocked_user_story = m.blocked_role_points.user_story + m.blocked_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.blocked_user_story.id) return m @@ -380,24 +399,28 @@ def test_user_story_put_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) us = f.UserStoryFactory.create(project=project1) + us = attach_userstory_extra_info(UserStory.objects.all()).get(id=us.id) url = reverse('userstories-detail', kwargs={"pk": us.pk}) @@ -592,7 +615,6 @@ def test_user_story_delete(client, data): assert results == [401, 403, 403, 451] - def test_user_story_action_bulk_create(client, data): url = reverse('userstories-bulk-create') @@ -746,7 +768,7 @@ def test_user_story_voters_retrieve(client, data): add_vote(data.blocked_user_story, data.project_owner) blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -840,16 +862,16 @@ def test_userstory_watchers_list(client, data): def test_userstory_watchers_retrieve(client, data): add_watcher(data.public_user_story, data.project_owner) public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_user_story1, data.project_owner) private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_user_story2, data.project_owner) private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_user_story, data.project_owner) blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py index afc3597d..34d4cf00 100644 --- a/tests/integration/resources_permissions/test_webhooks_resources.py +++ b/tests/integration/resources_permissions/test_webhooks_resources.py @@ -242,16 +242,19 @@ def test_webhook_action_test(client, data): ] with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', url1, None, users) assert results == [404, 404, 200] assert _send_request_mock.called is True with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', url2, None, users) assert results == [404, 404, 404] assert _send_request_mock.called is False with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', blocked_url, None, users) assert results == [404, 404, 451] assert _send_request_mock.called is False diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index ad28cc16..18562a8e 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -43,7 +43,7 @@ def test_update_milestone_with_userstories_list(client): form_data = { "name": "test", - "user_stories": [UserStorySerializer(us).data] + "user_stories": [{"id": us.id}] } client.login(user) diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 661b68ff..d8943e71 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -790,7 +790,7 @@ def test_watchers_assignation_for_issue(client): assert response.status_code == 400 issue = f.create_issue(project=project1, owner=user1) - data = dict(IssueSerializer(issue).data) + data = {} data["id"] = None data["version"] = None data["watchers"] = [user1.pk, user2.pk] @@ -802,8 +802,7 @@ def test_watchers_assignation_for_issue(client): # Test the impossible case when project is not # exists in create request, and validator works as expected issue = f.create_issue(project=project1, owner=user1) - data = dict(IssueSerializer(issue).data) - + data = {} data["id"] = None data["watchers"] = [user1.pk, user2.pk] data["project"] = None @@ -842,10 +841,11 @@ def test_watchers_assignation_for_task(client): assert response.status_code == 400 task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) - data = dict(TaskSerializer(task).data) - data["id"] = None - data["version"] = None - data["watchers"] = [user1.pk, user2.pk] + data = { + "id": None, + "version": None, + "watchers": [user1.pk, user2.pk] + } url = reverse("tasks-list") response = client.json.post(url, json.dumps(data)) @@ -854,11 +854,11 @@ def test_watchers_assignation_for_task(client): # Test the impossible case when project is not # exists in create request, and validator works as expected task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) - data = dict(TaskSerializer(task).data) - - data["id"] = None - data["watchers"] = [user1.pk, user2.pk] - data["project"] = None + data = { + "id": None, + "watchers": [user1.pk, user2.pk], + "project": None + } url = reverse("tasks-list") response = client.json.post(url, json.dumps(data)) @@ -894,10 +894,11 @@ def test_watchers_assignation_for_us(client): assert response.status_code == 400 us = f.create_userstory(project=project1, owner=user1, status__project=project1) - data = dict(UserStorySerializer(us).data) - data["id"] = None - data["version"] = None - data["watchers"] = [user1.pk, user2.pk] + data = { + "id": None, + "version": None, + "watchers": [user1.pk, user2.pk] + } url = reverse("userstories-list") response = client.json.post(url, json.dumps(data)) @@ -906,11 +907,11 @@ def test_watchers_assignation_for_us(client): # Test the impossible case when project is not # exists in create request, and validator works as expected us = f.create_userstory(project=project1, owner=user1, status__project=project1) - data = dict(UserStorySerializer(us).data) - - data["id"] = None - data["watchers"] = [user1.pk, user2.pk] - data["project"] = None + data = { + "id": None, + "watchers": [user1.pk, user2.pk], + "project": None + } url = reverse("userstories-list") response = client.json.post(url, json.dumps(data)) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 7eac9b06..10805f8e 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import copy import uuid import csv @@ -26,7 +25,6 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.userstories import services, models -from taiga.projects.userstories.serializers import UserStorySerializer from .. import factories as f @@ -108,7 +106,7 @@ def test_create_userstory_without_default_values(client): client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert response.data['status'] == None + assert response.data['status'] is None def test_api_delete_userstory(client): @@ -211,7 +209,7 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) us1 = f.create_userstory(project=project) us2 = f.create_userstory(project=project) - m1 = f.MilestoneFactory.create(project=project) + f.MilestoneFactory.create(project=project) m2 = f.MilestoneFactory.create() url = reverse("userstories-bulk-update-milestone") @@ -262,48 +260,53 @@ def test_update_userstory_points(client): f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project, user=user2, role=role2) - f.PointsFactory.create(project=project, value=None) - f.PointsFactory.create(project=project, value=1) + points1 = f.PointsFactory.create(project=project, value=None) + points2 = f.PointsFactory.create(project=project, value=1) points3 = f.PointsFactory.create(project=project, value=2) - us = f.UserStoryFactory.create(project=project,owner=user1, status__project=project, + us = f.UserStoryFactory.create(project=project, owner=user1, status__project=project, milestone__project=project) - usdata = UserStorySerializer(us).data url = reverse("userstories-detail", args=[us.pk]) client.login(user1) # invalid role - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({"222222": points3.pk}) + data = { + "version": us.version, + "points": { + str(role1.pk): points1.pk, + str(role2.pk): points2.pk, + "222222": points3.pk + } + } response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 # invalid point - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({str(role1.pk): "999999"}) + data = { + "version": us.version, + "points": { + str(role1.pk): 999999, + str(role2.pk): points2.pk + } + } response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 # Api should save successful - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({str(role1.pk): points3.pk}) + data = { + "version": us.version, + "points": { + str(role1.pk): points3.pk, + str(role2.pk): points2.pk + } + } response = client.json.patch(url, json.dumps(data)) - us = models.UserStory.objects.get(pk=us.pk) - usdatanew = UserStorySerializer(us).data - assert response.status_code == 200, str(response.content) - assert response.data["points"] == usdatanew['points'] - assert response.data["points"] != usdata['points'] + assert response.data["points"][str(role1.pk)] == points3.pk def test_update_userstory_rolepoints_on_add_new_role(client): @@ -438,32 +441,32 @@ def test_api_filters_data(client): # | 9 | user2 | user3 | tag0 | # ------------------------------------------------------ - user_story0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, - status=status3, tags=[tag1]) - user_story1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, - status=status3, tags=[tag2]) - user_story2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, - status=status1, tags=[tag1, tag2]) - user_story3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, - status=status0, tags=[tag3]) - user_story4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, - status=status0, tags=[tag1, tag2, tag3]) - user_story5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, - status=status2, tags=[tag3]) - user_story6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, - status=status3, tags=[tag1, tag2]) - user_story7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, - status=status0, tags=[tag3]) - user_story8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, - status=status3, tags=[tag1]) - user_story9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, - status=status1, tags=[tag0]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) url = reverse("userstories-filters-data") + "?project={}".format(project.id) client.login(user1) - ## No filter + # No filter response = client.get(url) assert response.status_code == 200 @@ -471,7 +474,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 4 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 @@ -486,7 +489,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 - ## Filter ((status0 or status3) + # Filter ((status0 or status3) response = client.get(url + "&status={},{}".format(status3.id, status0.id)) assert response.status_code == 200 @@ -494,7 +497,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 @@ -509,7 +512,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 - ## Filter ((tag1 and tag2) and (user1 or user2)) + # Filter ((tag1 and tag2) and (user1 or user2)) response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) assert response.status_code == 200 @@ -517,7 +520,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 0 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 @@ -556,7 +559,7 @@ def test_custom_fields_csv_generation(): attr = f.UserStoryCustomAttributeFactory.create(project=project, name="attr1", description="desc") us = f.UserStoryFactory.create(project=project) attr_values = us.custom_attributes_values - attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.attributes_values = {str(attr.id): "val1"} attr_values.save() queryset = project.user_stories.all() data = services.userstories_to_csv(project, queryset) @@ -595,7 +598,7 @@ def test_update_userstory_update_watchers(client): client.login(user=us.owner) url = reverse("userstories-detail", kwargs={"pk": us.pk}) - data = {"watchers": [watching_user.id], "version":1} + data = {"watchers": [watching_user.id], "version": 1} response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -614,7 +617,7 @@ def test_update_userstory_remove_watchers(client): client.login(user=us.owner) url = reverse("userstories-detail", kwargs={"pk": us.pk}) - data = {"watchers": [], "version":1} + data = {"watchers": [], "version": 1} response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -634,7 +637,7 @@ def test_update_userstory_update_tribe_gig(client): "id": 2, "title": "This is a gig test title" }, - "version":1 + "version": 1 } client.login(user=us.owner) diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py index 491ec5b4..8789408d 100644 --- a/tests/integration/test_webhooks_issues.py +++ b/tests/integration/test_webhooks_issues.py @@ -19,7 +19,6 @@ import pytest from unittest.mock import patch -from unittest.mock import Mock from .. import factories as f @@ -29,8 +28,6 @@ from taiga.projects.history import services pytestmark = pytest.mark.django_db(transaction=True) -from taiga.base.utils import json - def test_webhooks_when_create_issue(settings): settings.WEBHOOKS_ENABLED = True project = f.ProjectFactory() @@ -79,7 +76,7 @@ def test_webhooks_when_update_issue(settings): assert data["data"]["subject"] == obj.subject assert data["change"]["comment"] == "test_comment" assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] - assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] def test_webhooks_when_delete_issue(settings): diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index 349a912c..cc88552f 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -19,46 +19,43 @@ import pytest -from .. import factories as f from django.db import models -from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin -from taiga.projects.models import Project +from taiga.base.api.validators import ModelValidator +from taiga.projects.validators import DuplicatedNameInProjectValidator pytestmark = pytest.mark.django_db(transaction=True) -import factory - class AuxProjectModel(models.Model): pass + class AuxModelWithNameAttribute(models.Model): name = models.CharField(max_length=255, null=False, blank=False) project = models.ForeignKey(AuxProjectModel, null=False, blank=False) -class AuxSerializer(ValidateDuplicatedNameInProjectMixin): +class AuxValidator(DuplicatedNameInProjectValidator, ModelValidator): class Meta: model = AuxModelWithNameAttribute - def test_duplicated_name_validation(): project = AuxProjectModel.objects.create() - instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project) + AuxModelWithNameAttribute.objects.create(name="1", project=project) instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project) # No duplicated_name - serializer = AuxSerializer(data={"name": "3", "project": project.id}) + validator = AuxValidator(data={"name": "3", "project": project.id}) - assert serializer.is_valid() + assert validator.is_valid() # Create duplicated_name - serializer = AuxSerializer(data={"name": "1", "project": project.id}) + validator = AuxValidator(data={"name": "1", "project": project.id}) - assert not serializer.is_valid() + assert not validator.is_valid() # Update name to existing one - serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) + validator = AuxValidator(data={"id": instance_2.id, "name": "1", "project": project.id}) - assert not serializer.is_valid() + assert not validator.is_valid() From d7a979d23c4ad900e5a70ee9568129e23e84f217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 17:08:14 +0200 Subject: [PATCH 085/261] Migrating external apps --- taiga/external_apps/api.py | 10 +++--- taiga/external_apps/serializers.py | 45 +++++++++++-------------- taiga/external_apps/validators.py | 54 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 30 deletions(-) create mode 100644 taiga/external_apps/validators.py diff --git a/taiga/external_apps/api.py b/taiga/external_apps/api.py index 931337a8..8ded55d5 100644 --- a/taiga/external_apps/api.py +++ b/taiga/external_apps/api.py @@ -17,6 +17,7 @@ # along with this program. If not, see . from . import serializers +from . import validators from . import models from . import permissions from . import services @@ -27,12 +28,12 @@ from taiga.base.api import ModelCrudViewSet, ModelRetrieveViewSet from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route, detail_route -from django.db import transaction from django.utils.translation import ugettext_lazy as _ class Application(ModelRetrieveViewSet): serializer_class = serializers.ApplicationSerializer + validator_class = validators.ApplicationValidator permission_classes = (permissions.ApplicationPermission,) model = models.Application @@ -61,6 +62,7 @@ class Application(ModelRetrieveViewSet): class ApplicationToken(ModelCrudViewSet): serializer_class = serializers.ApplicationTokenSerializer + validator_class = validators.ApplicationTokenValidator permission_classes = (permissions.ApplicationTokenPermission,) def get_queryset(self): @@ -87,9 +89,9 @@ class ApplicationToken(ModelCrudViewSet): auth_code = request.DATA.get("auth_code", None) state = request.DATA.get("state", None) application_token = get_object_or_404(models.ApplicationToken, - application__id=application_id, - auth_code=auth_code, - state=state) + application__id=application_id, + auth_code=auth_code, + state=state) application_token.generate_token() application_token.save() diff --git a/taiga/external_apps/serializers.py b/taiga/external_apps/serializers.py index 095465fd..12ed3bab 100644 --- a/taiga/external_apps/serializers.py +++ b/taiga/external_apps/serializers.py @@ -16,9 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import json - from taiga.base.api import serializers +from taiga.base.fields import Field from . import models from . import services @@ -26,33 +25,27 @@ from . import services from django.utils.translation import ugettext as _ -class ApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = models.Application - fields = ("id", "name", "web", "description", "icon_url") +class ApplicationSerializer(serializers.LightSerializer): + id = Field() + name = Field() + web = Field() + description = Field() + icon_url = Field() -class ApplicationTokenSerializer(serializers.ModelSerializer): - cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) - next_url = serializers.CharField(source="next_url", read_only=True) - application = ApplicationSerializer(read_only=True) - - class Meta: - model = models.ApplicationToken - fields = ("user", "id", "application", "auth_code", "next_url") +class ApplicationTokenSerializer(serializers.LightSerializer): + id = Field() + user = Field(attr="user_id") + application = ApplicationSerializer() + auth_code = Field() + next_url = Field() -class AuthorizationCodeSerializer(serializers.ModelSerializer): - next_url = serializers.CharField(source="next_url", read_only=True) - class Meta: - model = models.ApplicationToken - fields = ("auth_code", "state", "next_url") +class AuthorizationCodeSerializer(serializers.LightSerializer): + state = Field() + auth_code = Field() + next_url = Field() -class AccessTokenSerializer(serializers.ModelSerializer): - cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) - next_url = serializers.CharField(source="next_url", read_only=True) - - class Meta: - model = models.ApplicationToken - fields = ("cyphered_token", ) +class AccessTokenSerializer(serializers.LightSerializer): + cyphered_token = Field() diff --git a/taiga/external_apps/validators.py b/taiga/external_apps/validators.py new file mode 100644 index 00000000..b2f2354d --- /dev/null +++ b/taiga/external_apps/validators.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import serializers + +from . import models +from taiga.base.api import validators + + +class ApplicationValidator(validators.ModelValidator): + class Meta: + model = models.Application + fields = ("id", "name", "web", "description", "icon_url") + + +class ApplicationTokenValidator(validators.ModelValidator): + cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) + next_url = serializers.CharField(source="next_url", read_only=True) + application = ApplicationValidator(read_only=True) + + class Meta: + model = models.ApplicationToken + fields = ("user", "id", "application", "auth_code", "next_url") + + +class AuthorizationCodeValidator(validators.ModelValidator): + next_url = serializers.CharField(source="next_url", read_only=True) + class Meta: + model = models.ApplicationToken + fields = ("auth_code", "state", "next_url") + + +class AccessTokenValidator(validators.ModelValidator): + cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) + next_url = serializers.CharField(source="next_url", read_only=True) + + class Meta: + model = models.ApplicationToken + fields = ("cyphered_token", ) From 39075288acf8e714326dc0446335436c8c67e30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 17:49:32 +0200 Subject: [PATCH 086/261] Migrating user storage --- taiga/userstorage/api.py | 18 +++++++++++------- taiga/userstorage/serializers.py | 16 ++++++---------- taiga/userstorage/validators.py | 27 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 taiga/userstorage/validators.py diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py index 62575d2b..5b097e71 100644 --- a/taiga/userstorage/api.py +++ b/taiga/userstorage/api.py @@ -17,14 +17,15 @@ # along with this program. If not, see . from django.utils.translation import ugettext as _ -from django.db import IntegrityError from taiga.base.api import ModelCrudViewSet +from taiga.base.api.serializers import ValidationError from taiga.base import exceptions as exc from . import models from . import filters from . import serializers +from . import validators from . import permissions @@ -32,6 +33,7 @@ class StorageEntriesViewSet(ModelCrudViewSet): model = models.StorageEntry filter_backends = (filters.StorageEntriesFilterBackend,) serializer_class = serializers.StorageEntrySerializer + validator_class = validators.StorageEntryValidator permission_classes = [permissions.StorageEntriesPermission] lookup_field = "key" @@ -45,9 +47,11 @@ class StorageEntriesViewSet(ModelCrudViewSet): obj.owner = self.request.user def create(self, *args, **kwargs): - try: - return super().create(*args, **kwargs) - except IntegrityError: - key = self.request.DATA.get("key", None) - raise exc.IntegrityError(_("Duplicate key value violates unique constraint. " - "Key '{}' already exists.").format(key)) + key = self.request.DATA.get("key", None) + if (key and self.request.user.is_authenticated() and + self.request.user.storage_entries.filter(key=key).exists()): + raise exc.BadRequest( + _("Duplicate key value violates unique constraint. " + "Key '{}' already exists.").format(key) + ) + return super().create(*args, **kwargs) diff --git a/taiga/userstorage/serializers.py b/taiga/userstorage/serializers.py index 5fd97692..38765f19 100644 --- a/taiga/userstorage/serializers.py +++ b/taiga/userstorage/serializers.py @@ -17,15 +17,11 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.base.fields import JsonField - -from . import models +from taiga.base.fields import Field -class StorageEntrySerializer(serializers.ModelSerializer): - value = JsonField(label="value") - - class Meta: - model = models.StorageEntry - fields = ("key", "value", "created_date", "modified_date") - read_only_fields = ("created_date", "modified_date") +class StorageEntrySerializer(serializers.LightSerializer): + key = Field() + value = Field() + created_date = Field() + modified_date = Field() diff --git a/taiga/userstorage/validators.py b/taiga/userstorage/validators.py new file mode 100644 index 00000000..615b88d7 --- /dev/null +++ b/taiga/userstorage/validators.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import validators + +from . import models + + +class StorageEntryValidator(validators.ModelValidator): + class Meta: + model = models.StorageEntry + fields = ("key", "value") From 7d2b6c34ce71dea1cc966bf1a90abe4f719e8693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 19:29:49 +0200 Subject: [PATCH 087/261] Migrating users serializers and validators --- taiga/base/utils/dicts.py | 4 + taiga/users/api.py | 73 ++++++----- taiga/users/serializers.py | 208 +++++++++++++------------------- taiga/users/validators.py | 82 ++++++++++++- tests/integration/test_users.py | 9 +- 5 files changed, 214 insertions(+), 162 deletions(-) diff --git a/taiga/base/utils/dicts.py b/taiga/base/utils/dicts.py index 23b90f17..bf3d2c71 100644 --- a/taiga/base/utils/dicts.py +++ b/taiga/base/utils/dicts.py @@ -25,3 +25,7 @@ def dict_sum(*args): assert isinstance(arg, dict) result += collections.Counter(arg) return result + + +def into_namedtuple(dictionary): + return collections.namedtuple('GenericDict', dictionary.keys())(**dictionary) diff --git a/taiga/users/api.py b/taiga/users/api.py index a02e1576..00d5d279 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -19,7 +19,6 @@ import uuid from django.apps import apps -from django.db.models import Q, F from django.utils.translation import ugettext as _ from django.core.validators import validate_email from django.core.exceptions import ValidationError @@ -28,21 +27,21 @@ from django.conf import settings from taiga.base import exceptions as exc from taiga.base import filters from taiga.base import response +from taiga.base.utils.dicts import into_namedtuple from taiga.auth.tokens import get_user_for_token from taiga.base.decorators import list_route from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.base.filters import PermissionBasedFilterBackend from taiga.base.api.utils import get_object_or_404 from taiga.base.filters import MembersFilterBackend from taiga.base.mails import mail_builder -from taiga.projects.votes import services as votes_service from taiga.users.services import get_user_by_username_or_email from easy_thumbnails.source_generators import pil_image from . import models from . import serializers +from . import validators from . import permissions from . import filters as user_filters from . import services @@ -53,6 +52,8 @@ class UsersViewSet(ModelCrudViewSet): permission_classes = (permissions.UserPermission,) admin_serializer_class = serializers.UserAdminSerializer serializer_class = serializers.UserSerializer + admin_validator_class = validators.UserAdminValidator + validator_class = validators.UserValidator queryset = models.User.objects.all().prefetch_related("memberships") filter_backends = (MembersFilterBackend,) @@ -64,6 +65,14 @@ class UsersViewSet(ModelCrudViewSet): return self.serializer_class + def get_validator_class(self): + if self.action in ["partial_update", "update", "retrieve", "by_username"]: + user = self.object + if self.request.user == user or self.request.user.is_superuser: + return self.admin_validator_class + + return self.validator_class + def create(self, *args, **kwargs): raise exc.NotSupported() @@ -86,7 +95,7 @@ class UsersViewSet(ModelCrudViewSet): serializer = self.get_serializer(self.object) return response.Ok(serializer.data) - #TODO: commit_on_success + # TODO: commit_on_success def partial_update(self, request, *args, **kwargs): """ We must detect if the user is trying to change his email so we can @@ -96,12 +105,10 @@ class UsersViewSet(ModelCrudViewSet): user = self.get_object() self.check_permissions(request, "update", user) - ret = super().partial_update(request, *args, **kwargs) - new_email = request.DATA.get('email', None) if new_email is not None: valid_new_email = True - duplicated_email = models.User.objects.filter(email = new_email).exists() + duplicated_email = models.User.objects.filter(email=new_email).exists() try: validate_email(new_email) @@ -115,14 +122,21 @@ class UsersViewSet(ModelCrudViewSet): elif not valid_new_email: raise exc.WrongArguments(_("Not valid email")) - #We need to generate a token for the email + # We need to generate a token for the email request.user.email_token = str(uuid.uuid1()) request.user.new_email = new_email request.user.save(update_fields=["email_token", "new_email"]) - email = mail_builder.change_email(request.user.new_email, {"user": request.user, - "lang": request.user.lang}) + email = mail_builder.change_email( + request.user.new_email, + { + "user": request.user, + "lang": request.user.lang + } + ) email.send() + ret = super().partial_update(request, *args, **kwargs) + return ret def destroy(self, request, pk=None): @@ -165,16 +179,16 @@ class UsersViewSet(ModelCrudViewSet): self.check_permissions(request, "change_password_from_recovery", None) - serializer = serializers.RecoverySerializer(data=request.DATA, many=False) - if not serializer.is_valid(): + validator = validators.RecoveryValidator(data=request.DATA, many=False) + if not validator.is_valid(): raise exc.WrongArguments(_("Token is invalid")) try: - user = models.User.objects.get(token=serializer.data["token"]) + user = models.User.objects.get(token=validator.data["token"]) except models.User.DoesNotExist: raise exc.WrongArguments(_("Token is invalid")) - user.set_password(serializer.data["password"]) + user.set_password(validator.data["password"]) user.token = None user.save(update_fields=["password", "token"]) @@ -247,13 +261,13 @@ class UsersViewSet(ModelCrudViewSet): """ Verify the email change to current logged user. """ - serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False) - if not serializer.is_valid(): + validator = validators.ChangeEmailValidator(data=request.DATA, many=False) + if not validator.is_valid(): raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you " "didn't use it before?")) try: - user = models.User.objects.get(email_token=serializer.data["email_token"]) + user = models.User.objects.get(email_token=validator.data["email_token"]) except models.User.DoesNotExist: raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you " "didn't use it before?")) @@ -280,14 +294,14 @@ class UsersViewSet(ModelCrudViewSet): """ Cancel an account via token """ - serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False) - if not serializer.is_valid(): + validator = validators.CancelAccountValidator(data=request.DATA, many=False) + if not validator.is_valid(): raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) try: max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None) - user = get_user_for_token(serializer.data["cancel_token"], "cancel_account", - max_age=max_age_cancel_account) + user = get_user_for_token(validator.data["cancel_token"], "cancel_account", + max_age=max_age_cancel_account) except exc.NotAuthenticated: raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) @@ -305,7 +319,7 @@ class UsersViewSet(ModelCrudViewSet): self.object_list = user_filters.ContactsFilterBackend().filter_queryset( user, request, self.get_queryset(), self).extra( - select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name") + select={"complete_user_name": "concat(full_name, username)"}).order_by("complete_user_name") page = self.paginate_queryset(self.object_list) if page is not None: @@ -349,10 +363,10 @@ class UsersViewSet(ModelCrudViewSet): for elem in elements: if elem["type"] == "project": # projects are liked objects - response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data ) + response_data.append(serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args_liked).data) else: # stories, tasks and issues are voted objects - response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data ) + response_data.append(serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args_voted).data) return response.Ok(response_data) @@ -374,7 +388,7 @@ class UsersViewSet(ModelCrudViewSet): "user_likes": services.get_liked_content_for_user(request.user), } - response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements] + response_data = [serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements] return response.Ok(response_data) @@ -397,17 +411,18 @@ class UsersViewSet(ModelCrudViewSet): "user_votes": services.get_voted_content_for_user(request.user), } - response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements] + response_data = [serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements] return response.Ok(response_data) -###################################################### -## Role -###################################################### +###################################################### +# Role +###################################################### class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Role serializer_class = serializers.RoleSerializer + validator_class = validators.RoleValidator permission_classes = (permissions.RolesPermission, ) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 3d584c53..75daa74e 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -22,7 +22,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers -from taiga.base.fields import PgArrayField, Field, MethodField +from taiga.base.fields import PgArrayField, Field, MethodField, I18NField from taiga.base.utils.thumbnails import get_thumbnail_url @@ -40,47 +40,28 @@ import re # User ###################################################### -class ContactProjectDetailSerializer(serializers.ModelSerializer): - class Meta: - model = Project - fields = ("id", "slug", "name") +class ContactProjectDetailSerializer(serializers.LightSerializer): + id = Field() + slug = Field() + name = Field() -class UserSerializer(serializers.ModelSerializer): - full_name_display = serializers.SerializerMethodField("get_full_name_display") - photo = serializers.SerializerMethodField("get_photo") - big_photo = serializers.SerializerMethodField("get_big_photo") - gravatar_url = serializers.SerializerMethodField("get_gravatar_url") - roles = serializers.SerializerMethodField("get_roles") - projects_with_me = serializers.SerializerMethodField("get_projects_with_me") - - class Meta: - model = User - # 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", "lang", "theme", "timezone", "is_active", - "photo", "big_photo", "roles", "projects_with_me", - "gravatar_url") - read_only_fields = ("id",) - - def validate_username(self, attrs, source): - value = attrs[source] - 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'")) - - if (self.object and - self.object.username != value and - User.objects.filter(username=value).exists()): - raise serializers.ValidationError(_("Invalid username. Try with a different one.")) - - return attrs +class UserSerializer(serializers.LightSerializer): + id = Field() + username = Field() + full_name = Field() + full_name_display = MethodField() + color = Field() + bio = Field() + lang = Field() + theme = Field() + timezone = Field() + is_active = Field() + photo = MethodField() + big_photo = MethodField() + gravatar_url = MethodField() + roles = MethodField() + projects_with_me = MethodField() def get_full_name_display(self, obj): return obj.get_full_name() if obj else "" @@ -113,24 +94,13 @@ class UserSerializer(serializers.ModelSerializer): class UserAdminSerializer(UserSerializer): - total_private_projects = serializers.SerializerMethodField("get_total_private_projects") - total_public_projects = serializers.SerializerMethodField("get_total_public_projects") - - class Meta: - model = User - # 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", "lang", "theme", "timezone", "is_active", "photo", - "big_photo", "gravatar_url", - "max_private_projects", "max_public_projects", - "max_memberships_private_projects", "max_memberships_public_projects", - "total_private_projects", "total_public_projects") - - read_only_fields = ("id", "email", - "max_private_projects", "max_public_projects", - "max_memberships_private_projects", - "max_memberships_public_projects") + total_private_projects = MethodField() + total_public_projects = MethodField() + email = Field() + max_private_projects = Field() + max_public_projects = Field() + max_memberships_private_projects = Field() + max_memberships_public_projects = Field() def get_total_private_projects(self, user): return user.owned_projects.filter(is_private=True).count() @@ -163,75 +133,63 @@ class UserBasicInfoSerializer(serializers.LightSerializer): return super().to_value(instance) -class RecoverySerializer(serializers.Serializer): - token = serializers.CharField(max_length=200) - password = serializers.CharField(min_length=6) - - -class ChangeEmailSerializer(serializers.Serializer): - email_token = serializers.CharField(max_length=200) - - -class CancelAccountSerializer(serializers.Serializer): - cancel_token = serializers.CharField(max_length=200) - - ###################################################### # Role ###################################################### -class RoleSerializer(serializers.ModelSerializer): - members_count = serializers.SerializerMethodField("get_members_count") +class RoleSerializer(serializers.LightSerializer): + id = Field() + name = Field() + computable = Field() + project = Field(attr="project_id") + order = Field() + members_count = MethodField() permissions = PgArrayField(required=False) - class Meta: - model = Role - fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count') - i18n_fields = ("name",) - def get_members_count(self, obj): return obj.memberships.count() -class ProjectRoleSerializer(serializers.ModelSerializer): - class Meta: - model = Role - fields = ('id', 'name', 'slug', 'order', 'computable') - i18n_fields = ("name",) +class ProjectRoleSerializer(serializers.LightSerializer): + id = Field() + name = I18NField() + slug = Field() + order = Field() + computable = Field() ###################################################### # Like ###################################################### -class HighLightedContentSerializer(serializers.Serializer): - type = serializers.CharField() - id = serializers.IntegerField() - ref = serializers.IntegerField() - slug = serializers.CharField() - name = serializers.CharField() - subject = serializers.CharField() - description = serializers.SerializerMethodField("get_description") - assigned_to = serializers.IntegerField() - status = serializers.CharField() - status_color = serializers.CharField() - tags_colors = serializers.SerializerMethodField("get_tags_color") - created_date = serializers.DateTimeField() - is_private = serializers.SerializerMethodField("get_is_private") - logo_small_url = serializers.SerializerMethodField("get_logo_small_url") +class HighLightedContentSerializer(serializers.LightSerializer): + type = Field() + id = Field() + ref = Field() + slug = Field() + name = Field() + subject = Field() + description = MethodField() + assigned_to = Field() + status = Field() + status_color = Field() + tags_colors = MethodField() + created_date = Field() + is_private = MethodField() + logo_small_url = MethodField() - project = serializers.SerializerMethodField("get_project") - project_name = serializers.SerializerMethodField("get_project_name") - project_slug = serializers.SerializerMethodField("get_project_slug") - project_is_private = serializers.SerializerMethodField("get_project_is_private") - project_blocked_code = serializers.CharField() + project = MethodField() + project_name = MethodField() + project_slug = MethodField() + project_is_private = MethodField() + project_blocked_code = Field() - assigned_to_username = serializers.CharField() - assigned_to_full_name = serializers.CharField() - assigned_to_photo = serializers.SerializerMethodField("get_photo") + assigned_to_username = Field() + assigned_to_full_name = Field() + assigned_to_photo = MethodField() - is_watcher = serializers.SerializerMethodField("get_is_watcher") - total_watchers = serializers.IntegerField() + is_watcher = MethodField() + total_watchers = Field() def __init__(self, *args, **kwargs): # Don't pass the extra ids args up to the superclass @@ -241,18 +199,18 @@ class HighLightedContentSerializer(serializers.Serializer): super().__init__(*args, **kwargs) def _none_if_project(self, obj, property): - type = obj.get("type", "") + type = getattr(obj, "type", "") if type == "project": return None - return obj.get(property) + return getattr(obj, property) def _none_if_not_project(self, obj, property): - type = obj.get("type", "") + type = getattr(obj, "type", "") if type != "project": return None - return obj.get(property) + return getattr(obj, property) def get_project(self, obj): return self._none_if_project(obj, "project") @@ -278,29 +236,29 @@ class HighLightedContentSerializer(serializers.Serializer): return get_thumbnail_url(logo, settings.THN_LOGO_SMALL) return None - def get_photo(self, obj): - type = obj.get("type", "") + def get_assigned_to_photo(self, obj): + type = getattr(obj, "type", "") if type == "project": return None UserData = namedtuple("UserData", ["photo", "email"]) - user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "") + user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "") return get_photo_or_gravatar_url(user_data) - def get_tags_color(self, obj): - tags = obj.get("tags", []) + def get_tags_colors(self, obj): + tags = getattr(obj, "tags", []) tags = tags if tags is not None else [] - tags_colors = obj.get("tags_colors", []) + tags_colors = getattr(obj, "tags_colors", []) tags_colors = tags_colors if tags_colors is not None else [] return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags] def get_is_watcher(self, obj): - return obj["id"] in self.user_watching.get(obj["type"], []) + return obj.id in self.user_watching.get(obj.type, []) class LikedObjectSerializer(HighLightedContentSerializer): - is_fan = serializers.SerializerMethodField("get_is_fan") - total_fans = serializers.IntegerField() + is_fan = MethodField() + total_fans = Field() def __init__(self, *args, **kwargs): # Don't pass the extra ids args up to the superclass @@ -310,12 +268,12 @@ class LikedObjectSerializer(HighLightedContentSerializer): super().__init__(*args, **kwargs) def get_is_fan(self, obj): - return obj["id"] in self.user_likes.get(obj["type"], []) + return obj.id in self.user_likes.get(obj.type, []) class VotedObjectSerializer(HighLightedContentSerializer): - is_voter = serializers.SerializerMethodField("get_is_voter") - total_voters = serializers.IntegerField() + is_voter = MethodField() + total_voters = Field() def __init__(self, *args, **kwargs): # Don't pass the extra ids args up to the superclass @@ -325,4 +283,4 @@ class VotedObjectSerializer(HighLightedContentSerializer): super().__init__(*args, **kwargs) def get_is_voter(self, obj): - return obj["id"] in self.user_votes.get(obj["type"], []) + return obj.id in self.user_votes.get(obj.type, []) diff --git a/taiga/users/validators.py b/taiga/users/validators.py index 477342de..11e78efb 100644 --- a/taiga/users/validators.py +++ b/taiga/users/validators.py @@ -3,7 +3,6 @@ # Copyright (C) 2014-2016 Jesús Espino # Copyright (C) 2014-2016 David Barragán # Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Anler Hernández # 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 @@ -17,17 +16,92 @@ # 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.core import validators as core_validators +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.fields import PgArrayField, Field -from . import models +from .models import User, Role + +import re class RoleExistsValidator: def validate_role_id(self, attrs, source): value = attrs[source] - if not models.Role.objects.filter(pk=value).exists(): + if not Role.objects.filter(pk=value).exists(): msg = _("There's no role with that id") raise serializers.ValidationError(msg) return attrs + + +###################################################### +# User +###################################################### +class UserValidator(validators.ModelValidator): + class Meta: + model = User + fields = ("username", "full_name", "color", "bio", "lang", + "theme", "timezone", "is_active") + + def validate_username(self, attrs, source): + value = attrs[source] + validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), + _("invalid")) + + try: + validator(value) + except ValidationError: + raise validators.ValidationError(_("Required. 255 characters or fewer. Letters, " + "numbers and /./-/_ characters'")) + + if (self.object and + self.object.username != value and + User.objects.filter(username=value).exists()): + raise validators.ValidationError(_("Invalid username. Try with a different one.")) + + return attrs + + +class UserAdminValidator(UserValidator): + class Meta: + model = User + # IMPORTANT: Maintain the UserSerializer Meta up to date + # with this info (including here the email) + fields = ("username", "full_name", "color", "bio", "lang", + "theme", "timezone", "is_active", "email") + + +class RecoveryValidator(validators.Validator): + token = serializers.CharField(max_length=200) + password = serializers.CharField(min_length=6) + + +class ChangeEmailValidator(validators.Validator): + email_token = serializers.CharField(max_length=200) + + +class CancelAccountValidator(validators.Validator): + cancel_token = serializers.CharField(max_length=200) + + +###################################################### +# Role +###################################################### + +class RoleValidator(validators.ModelValidator): + permissions = PgArrayField(required=False) + + class Meta: + model = Role + fields = ('id', 'name', 'permissions', 'computable', 'project', 'order') + i18n_fields = ("name",) + + +class ProjectRoleValidator(validators.ModelValidator): + class Meta: + model = Role + fields = ('id', 'name', 'slug', 'order', 'computable') diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 90ed5599..f4bf2b51 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -30,6 +30,7 @@ from ..utils import DUMMY_BMP_DATA from taiga.base.utils import json from taiga.base.utils.thumbnails import get_thumbnail_url +from taiga.base.utils.dicts import into_namedtuple from taiga.users import models from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer from taiga.auth.tokens import get_token_for_user @@ -505,7 +506,7 @@ def test_get_watched_list_valid_info_for_project(): raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0] - project_watch_info = LikedObjectSerializer(raw_project_watch_info).data + project_watch_info = LikedObjectSerializer(into_namedtuple(raw_project_watch_info)).data assert project_watch_info["type"] == "project" assert project_watch_info["id"] == project.id @@ -559,7 +560,7 @@ def test_get_liked_list_valid_info(): project.refresh_totals() raw_project_like_info = get_liked_list(fan_user, viewer_user)[0] - project_like_info = LikedObjectSerializer(raw_project_like_info).data + project_like_info = LikedObjectSerializer(into_namedtuple(raw_project_like_info)).data assert project_like_info["type"] == "project" assert project_like_info["id"] == project.id @@ -609,7 +610,7 @@ def test_get_watched_list_valid_info_for_not_project_types(): instance.add_watcher(fav_user) raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0] - instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data + instance_watch_info = VotedObjectSerializer(into_namedtuple(raw_instance_watch_info)).data assert instance_watch_info["type"] == object_type assert instance_watch_info["id"] == instance.id @@ -666,7 +667,7 @@ def test_get_voted_list_valid_info(): f.VotesFactory(content_type=content_type, object_id=instance.id, count=3) raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0] - instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data + instance_vote_info = VotedObjectSerializer(into_namedtuple(raw_instance_vote_info)).data assert instance_vote_info["type"] == object_type assert instance_vote_info["id"] == instance.id From cbec0caca24840e419c59eeeab63afb9c3671a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 19:59:49 +0200 Subject: [PATCH 088/261] Migrating tagging validators --- taiga/projects/tagging/api.py | 36 +++++++++---------- .../tagging/{serializers.py => validators.py} | 27 +++++++------- 2 files changed, 31 insertions(+), 32 deletions(-) rename taiga/projects/tagging/{serializers.py => validators.py} (77%) diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py index c2dbd38a..db57b946 100644 --- a/taiga/projects/tagging/api.py +++ b/taiga/projects/tagging/api.py @@ -21,7 +21,7 @@ from taiga.base.decorators import detail_route from taiga.base.utils.collections import OrderedSet from . import services -from . import serializers +from . import validators class TagsColorsResourceMixin: @@ -38,27 +38,26 @@ class TagsColorsResourceMixin: self.check_permissions(request, "create_tag", project) self._raise_if_blocked(project) - serializer = serializers.CreateTagSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.CreateTagValidator(data=request.DATA, project=project) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data services.create_tag(project, data.get("tag"), data.get("color")) return response.Ok() - @detail_route(methods=["POST"]) def edit_tag(self, request, pk=None): project = self.get_object() self.check_permissions(request, "edit_tag", project) self._raise_if_blocked(project) - serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.EditTagTagValidator(data=request.DATA, project=project) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data services.edit_tag(project, data.get("from_tag"), to_tag=data.get("to_tag", None), @@ -66,18 +65,17 @@ class TagsColorsResourceMixin: return response.Ok() - @detail_route(methods=["POST"]) def delete_tag(self, request, pk=None): project = self.get_object() self.check_permissions(request, "delete_tag", project) self._raise_if_blocked(project) - serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.DeleteTagValidator(data=request.DATA, project=project) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data services.delete_tag(project, data.get("tag")) return response.Ok() @@ -88,11 +86,11 @@ class TagsColorsResourceMixin: self.check_permissions(request, "mix_tags", project) self._raise_if_blocked(project) - serializer = serializers.MixTagsSerializer(data=request.DATA, project=project) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.MixTagsValidator(data=request.DATA, project=project) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data services.mix_tags(project, data.get("from_tags"), data.get("to_tag")) return response.Ok() diff --git a/taiga/projects/tagging/serializers.py b/taiga/projects/tagging/validators.py similarity index 77% rename from taiga/projects/tagging/serializers.py rename to taiga/projects/tagging/validators.py index dc25b73a..595a5a3f 100644 --- a/taiga/projects/tagging/serializers.py +++ b/taiga/projects/tagging/validators.py @@ -19,6 +19,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers +from taiga.base.api import validators from . import services from . import fields @@ -26,7 +27,7 @@ from . import fields import re -class ProjectTagSerializer(serializers.Serializer): +class ProjectTagValidator(validators.Validator): def __init__(self, *args, **kwargs): # Don't pass the extra project arg self.project = kwargs.pop("project") @@ -35,26 +36,26 @@ class ProjectTagSerializer(serializers.Serializer): super().__init__(*args, **kwargs) -class CreateTagSerializer(ProjectTagSerializer): +class CreateTagValidator(ProjectTagValidator): tag = serializers.CharField() color = serializers.CharField(required=False) def validate_tag(self, attrs, source): tag = attrs.get(source, None) if services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag exists.")) + raise validators.ValidationError(_("The tag exists.")) return attrs def validate_color(self, attrs, source): color = attrs.get(source, None) if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): - raise serializers.ValidationError(_("The color is not a valid HEX color.")) + raise validators.ValidationError(_("The color is not a valid HEX color.")) return attrs -class EditTagTagSerializer(ProjectTagSerializer): +class EditTagTagValidator(ProjectTagValidator): from_tag = serializers.CharField() to_tag = serializers.CharField(required=False) color = serializers.CharField(required=False) @@ -62,37 +63,37 @@ class EditTagTagSerializer(ProjectTagSerializer): def validate_from_tag(self, attrs, source): tag = attrs.get(source, None) if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) + raise validators.ValidationError(_("The tag doesn't exist.")) return attrs def validate_to_tag(self, attrs, source): tag = attrs.get(source, None) if services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag exists yet")) + raise validators.ValidationError(_("The tag exists yet")) return attrs def validate_color(self, attrs, source): color = attrs.get(source, None) if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): - raise serializers.ValidationError(_("The color is not a valid HEX color.")) + raise validators.ValidationError(_("The color is not a valid HEX color.")) return attrs -class DeleteTagSerializer(ProjectTagSerializer): +class DeleteTagValidator(ProjectTagValidator): tag = serializers.CharField() def validate_tag(self, attrs, source): tag = attrs.get(source, None) if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) + raise validators.ValidationError(_("The tag doesn't exist.")) return attrs -class MixTagsSerializer(ProjectTagSerializer): +class MixTagsValidator(ProjectTagValidator): from_tags = fields.TagsField() to_tag = serializers.CharField() @@ -100,13 +101,13 @@ class MixTagsSerializer(ProjectTagSerializer): tags = attrs.get(source, None) for tag in tags: if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) + raise validators.ValidationError(_("The tag doesn't exist.")) return attrs def validate_to_tag(self, attrs, source): tag = attrs.get(source, None) if not services.tag_exist_for_project_elements(self.project, tag): - raise serializers.ValidationError(_("The tag doesn't exist.")) + raise validators.ValidationError(_("The tag doesn't exist.")) return attrs From a51ca8c85af5d95b8d39d175ecddc32f25010b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 20:29:59 +0200 Subject: [PATCH 089/261] Migrating Likes and votes serializers --- taiga/projects/likes/serializers.py | 14 +++++++------- taiga/projects/votes/serializers.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/taiga/projects/likes/serializers.py b/taiga/projects/likes/serializers.py index 6a654705..ef058e70 100644 --- a/taiga/projects/likes/serializers.py +++ b/taiga/projects/likes/serializers.py @@ -17,14 +17,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.auth import get_user_model - from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField -class FanSerializer(serializers.ModelSerializer): - full_name = serializers.CharField(source='get_full_name', required=False) +class FanSerializer(serializers.LightSerializer): + id = Field() + username = Field() + full_name = MethodField() - class Meta: - model = get_user_model() - fields = ('id', 'username', 'full_name') + def get_full_name(self, obj): + return obj.get_full_name() diff --git a/taiga/projects/votes/serializers.py b/taiga/projects/votes/serializers.py index eb47c9ef..b97bd3bf 100644 --- a/taiga/projects/votes/serializers.py +++ b/taiga/projects/votes/serializers.py @@ -17,14 +17,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.auth import get_user_model - from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField -class VoterSerializer(serializers.ModelSerializer): - full_name = serializers.CharField(source='get_full_name', required=False) +class VoterSerializer(serializers.LightSerializer): + id = Field() + username = Field() + full_name = MethodField() - class Meta: - model = get_user_model() - fields = ('id', 'username', 'full_name') + def get_full_name(self, obj): + return obj.get_full_name() From 4f5a4f13145c97d5c4dfa7ce1f5bc2122da112e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 21:22:49 +0200 Subject: [PATCH 090/261] Migrating references validators --- taiga/projects/references/api.py | 10 +++++----- .../references/{serializers.py => validators.py} | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) rename taiga/projects/references/{serializers.py => validators.py} (95%) diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index 42d7f5a6..ff114ac6 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -24,7 +24,7 @@ from taiga.base.api import viewsets from taiga.base.api.utils import get_object_or_404 from taiga.permissions.services import user_has_perm -from .serializers import ResolverSerializer +from .validators import ResolverValidator from . import permissions @@ -32,11 +32,11 @@ class ResolverViewSet(viewsets.ViewSet): permission_classes = (permissions.ResolverPermission,) def list(self, request, **kwargs): - serializer = ResolverSerializer(data=request.QUERY_PARAMS) - if not serializer.is_valid(): - raise exc.BadRequest(serializer.errors) + validator = ResolverValidator(data=request.QUERY_PARAMS) + if not validator.is_valid(): + raise exc.BadRequest(validator.errors) - data = serializer.data + data = validator.data project_model = apps.get_model("projects", "Project") project = get_object_or_404(project_model, slug=data["project"]) diff --git a/taiga/projects/references/serializers.py b/taiga/projects/references/validators.py similarity index 95% rename from taiga/projects/references/serializers.py rename to taiga/projects/references/validators.py index fb9ad177..5fcefee8 100644 --- a/taiga/projects/references/serializers.py +++ b/taiga/projects/references/validators.py @@ -17,9 +17,10 @@ # along with this program. If not, see . from taiga.base.api import serializers +from taiga.base.api import validators -class ResolverSerializer(serializers.Serializer): +class ResolverValidator(validators.Validator): project = serializers.CharField(max_length=512, required=True) milestone = serializers.CharField(max_length=512, required=False) us = serializers.IntegerField(required=False) From 04102e3b9f417cc2d06e33c065d1166054d1145a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 23:25:42 +0200 Subject: [PATCH 091/261] Migrating attachments serializers --- taiga/base/fields.py | 17 +++++++++--- taiga/projects/attachments/api.py | 2 ++ taiga/projects/attachments/serializers.py | 31 ++++++++++++--------- taiga/projects/attachments/validators.py | 33 +++++++++++++++++++++++ 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 taiga/projects/attachments/validators.py diff --git a/taiga/base/fields.py b/taiga/base/fields.py index f0cf4ee2..30be6b60 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -22,9 +22,11 @@ from taiga.base.api import serializers import serpy + #################################################################### -# Serializer fields +# DRF Serializer fields (OLD) #################################################################### +# NOTE: This should be in other place, for example taiga.base.api.serializers class JsonField(serializers.WritableField): @@ -74,6 +76,10 @@ class WatchersField(serializers.WritableField): return data +#################################################################### +# Serpy fields (NEW) +#################################################################### + class Field(serpy.Field): pass @@ -82,13 +88,13 @@ class MethodField(serpy.MethodField): pass -class I18NField(serpy.Field): +class I18NField(Field): def to_value(self, value): ret = super(I18NField, self).to_value(value) return _(ret) -class I18NJsonField(serpy.Field): +class I18NJsonField(Field): """ Json objects serializer. """ @@ -118,3 +124,8 @@ class I18NJsonField(serpy.Field): def to_native(self, obj): i18n_obj = self.translate_values(obj) return i18n_obj + + +class FileField(Field): + def to_value(self, value): + return value.name diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index f7b223e2..27f7ebe1 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -34,6 +34,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin from . import permissions from . import serializers +from . import validators from . import models @@ -42,6 +43,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, model = models.Attachment serializer_class = serializers.AttachmentSerializer + validator_class = validators.AttachmentValidator filter_fields = ["project", "object_id"] content_type = None diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 45e6be45..ce8893b7 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -19,24 +19,29 @@ from django.conf import settings from taiga.base.api import serializers -from taiga.base.fields import MethodField +from taiga.base.fields import MethodField, Field, FileField from taiga.base.utils.thumbnails import get_thumbnail_url from . import services -from . import models -class AttachmentSerializer(serializers.ModelSerializer): - url = serializers.SerializerMethodField("get_url") - thumbnail_card_url = serializers.SerializerMethodField("get_thumbnail_card_url") - attached_file = serializers.FileField(required=True) - - class Meta: - model = models.Attachment - fields = ("id", "project", "owner", "name", "attached_file", "size", - "url", "thumbnail_card_url", "description", "is_deprecated", - "created_date", "modified_date", "object_id", "order", "sha1") - read_only_fields = ("owner", "created_date", "modified_date", "sha1") +class AttachmentSerializer(serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + owner = Field(attr="owner_id") + name = Field() + attached_file = FileField() + size = Field() + url = Field() + description = Field() + is_deprecated = Field() + created_date = Field() + modified_date = Field() + object_id = Field() + order = Field() + sha1 = Field() + url = MethodField("get_url") + thumbnail_card_url = MethodField("get_thumbnail_card_url") def get_url(self, obj): return obj.attached_file.url diff --git a/taiga/projects/attachments/validators.py b/taiga/projects/attachments/validators.py new file mode 100644 index 00000000..72355ce4 --- /dev/null +++ b/taiga/projects/attachments/validators.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import serializers +from taiga.base.api import validators + +from . import models + + +class AttachmentValidator(validators.ModelValidator): + attached_file = serializers.FileField(required=True) + + class Meta: + model = models.Attachment + fields = ("id", "project", "owner", "name", "attached_file", "size", + "description", "is_deprecated", "created_date", + "modified_date", "object_id", "order", "sha1") + read_only_fields = ("owner", "created_date", "modified_date", "sha1") From 8d86c42fa078adb58145a50469abdd4ddc8ba7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 23:48:33 +0200 Subject: [PATCH 092/261] Migrating feedback serializers --- taiga/feedback/api.py | 14 +++++++------- taiga/feedback/{serializers.py => validators.py} | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) rename taiga/feedback/{serializers.py => validators.py} (91%) diff --git a/taiga/feedback/api.py b/taiga/feedback/api.py index c477b5eb..0f573b87 100644 --- a/taiga/feedback/api.py +++ b/taiga/feedback/api.py @@ -20,7 +20,7 @@ from taiga.base import response from taiga.base.api import viewsets from . import permissions -from . import serializers +from . import validators from . import services import copy @@ -28,7 +28,7 @@ import copy class FeedbackViewSet(viewsets.ViewSet): permission_classes = (permissions.FeedbackPermission,) - serializer_class = serializers.FeedbackEntrySerializer + validator_class = validators.FeedbackEntryValidator def create(self, request, **kwargs): self.check_permissions(request, "create", None) @@ -37,11 +37,11 @@ class FeedbackViewSet(viewsets.ViewSet): data.update({"full_name": request.user.get_full_name(), "email": request.user.email}) - serializer = self.serializer_class(data=data) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = self.validator_class(data=data) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - self.object = serializer.save(force_insert=True) + self.object = validator.save(force_insert=True) extra = { "HTTP_HOST": request.META.get("HTTP_HOST", None), @@ -50,4 +50,4 @@ class FeedbackViewSet(viewsets.ViewSet): } services.send_feedback(self.object, extra, reply_to=[request.user.email]) - return response.Ok(serializer.data) + return response.Ok(validator.data) diff --git a/taiga/feedback/serializers.py b/taiga/feedback/validators.py similarity index 91% rename from taiga/feedback/serializers.py rename to taiga/feedback/validators.py index 1b5f1a3e..7b31ec88 100644 --- a/taiga/feedback/serializers.py +++ b/taiga/feedback/validators.py @@ -16,11 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api import serializers +from taiga.base.api import validators from . import models -class FeedbackEntrySerializer(serializers.ModelSerializer): +class FeedbackEntryValidator(validators.ModelValidator): class Meta: model = models.FeedbackEntry From 81454426f9b721fedac81e6b14bce0d9265265dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 6 Jul 2016 08:58:25 +0200 Subject: [PATCH 093/261] Migrating auth serializers --- taiga/auth/api.py | 27 ++++++++++---------- taiga/auth/{serializers.py => validators.py} | 13 +++++----- 2 files changed, 21 insertions(+), 19 deletions(-) rename taiga/auth/{serializers.py => validators.py} (81%) diff --git a/taiga/auth/api.py b/taiga/auth/api.py index 5d14d18f..df077b52 100644 --- a/taiga/auth/api.py +++ b/taiga/auth/api.py @@ -22,15 +22,16 @@ from enum import Enum from django.utils.translation import ugettext as _ from django.conf import settings +from taiga.base.api import validators 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 from taiga.base import response -from .serializers import PublicRegisterSerializer -from .serializers import PrivateRegisterForExistingUserSerializer -from .serializers import PrivateRegisterForNewUserSerializer +from .validators import PublicRegisterValidator +from .validators import PrivateRegisterForExistingUserValidator +from .validators import PrivateRegisterForNewUserValidator from .services import private_register_for_existing_user from .services import private_register_for_new_user @@ -44,7 +45,7 @@ from .permissions import AuthPermission def _parse_data(data:dict, *, cls): """ Generic function for parse user data using - specified serializer on `cls` keyword parameter. + specified validator on `cls` keyword parameter. Raises: RequestValidationError exception if some errors found when data is validated. @@ -52,21 +53,21 @@ def _parse_data(data:dict, *, cls): Returns the parsed data. """ - serializer = cls(data=data) - if not serializer.is_valid(): - raise exc.RequestValidationError(serializer.errors) - return serializer.data + validator = cls(data=data) + if not validator.is_valid(): + raise exc.RequestValidationError(validator.errors) + return validator.data # Parse public register data -parse_public_register_data = partial(_parse_data, cls=PublicRegisterSerializer) +parse_public_register_data = partial(_parse_data, cls=PublicRegisterValidator) # Parse private register data for existing user parse_private_register_for_existing_user_data = \ - partial(_parse_data, cls=PrivateRegisterForExistingUserSerializer) + partial(_parse_data, cls=PrivateRegisterForExistingUserValidator) # Parse private register data for new user parse_private_register_for_new_user_data = \ - partial(_parse_data, cls=PrivateRegisterForNewUserSerializer) + partial(_parse_data, cls=PrivateRegisterForNewUserValidator) class RegisterTypeEnum(Enum): @@ -81,10 +82,10 @@ def parse_register_type(userdata:dict) -> str: """ # Create adhoc inner serializer for avoid parse # manually the user data. - class _serializer(serializers.Serializer): + class _validator(validators.Validator): existing = serializers.BooleanField() - instance = _serializer(data=userdata) + instance = _validator(data=userdata) if not instance.is_valid(): raise exc.RequestValidationError(instance.errors) diff --git a/taiga/auth/serializers.py b/taiga/auth/validators.py similarity index 81% rename from taiga/auth/serializers.py rename to taiga/auth/validators.py index 8e8df4e2..6c3661ed 100644 --- a/taiga/auth/serializers.py +++ b/taiga/auth/validators.py @@ -16,16 +16,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.core import validators +from django.core import validators as core_validators from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from taiga.base.api import serializers +from taiga.base.api import validators import re -class BaseRegisterSerializer(serializers.Serializer): +class BaseRegisterValidator(validators.Validator): full_name = serializers.CharField(max_length=256) email = serializers.EmailField(max_length=255) username = serializers.CharField(max_length=255) @@ -33,7 +34,7 @@ class BaseRegisterSerializer(serializers.Serializer): def validate_username(self, attrs, source): value = attrs[source] - validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid") + validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid") try: validator(value) @@ -43,15 +44,15 @@ class BaseRegisterSerializer(serializers.Serializer): return attrs -class PublicRegisterSerializer(BaseRegisterSerializer): +class PublicRegisterValidator(BaseRegisterValidator): pass -class PrivateRegisterForNewUserSerializer(BaseRegisterSerializer): +class PrivateRegisterForNewUserValidator(BaseRegisterValidator): token = serializers.CharField(max_length=255, required=True) -class PrivateRegisterForExistingUserSerializer(serializers.Serializer): +class PrivateRegisterForExistingUserValidator(validators.Validator): username = serializers.CharField(max_length=255) password = serializers.CharField(min_length=4) token = serializers.CharField(max_length=255, required=True) From 6e3eeb7cca20eac0a931b6e384c82230e6f0a772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 6 Jul 2016 09:55:24 +0200 Subject: [PATCH 094/261] Migrating custom fields serializer --- taiga/projects/custom_attributes/api.py | 7 + .../projects/custom_attributes/serializers.py | 122 +++------------ .../projects/custom_attributes/validators.py | 146 ++++++++++++++++++ 3 files changed, 174 insertions(+), 101 deletions(-) create mode 100644 taiga/projects/custom_attributes/validators.py diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index 9bfc774f..2d05d186 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -32,6 +32,7 @@ from taiga.projects.occ.mixins import OCCResourceMixin from . import models from . import serializers +from . import validators from . import permissions from . import services @@ -43,6 +44,7 @@ from . import services class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.UserStoryCustomAttribute serializer_class = serializers.UserStoryCustomAttributeSerializer + validator_class = validators.UserStoryCustomAttributeValidator permission_classes = (permissions.UserStoryCustomAttributePermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -54,6 +56,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixi class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.TaskCustomAttribute serializer_class = serializers.TaskCustomAttributeSerializer + validator_class = validators.TaskCustomAttributeValidator permission_classes = (permissions.TaskCustomAttributePermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -65,6 +68,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, Mo class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.IssueCustomAttribute serializer_class = serializers.IssueCustomAttributeSerializer + validator_class = validators.IssueCustomAttributeValidator permission_classes = (permissions.IssueCustomAttributePermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project",) @@ -86,6 +90,7 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.UserStoryCustomAttributesValues serializer_class = serializers.UserStoryCustomAttributesValuesSerializer + validator_class = validators.UserStoryCustomAttributesValuesValidator permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,) lookup_field = "user_story_id" content_object = "user_story" @@ -99,6 +104,7 @@ class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.TaskCustomAttributesValues serializer_class = serializers.TaskCustomAttributesValuesSerializer + validator_class = validators.TaskCustomAttributesValuesValidator permission_classes = (permissions.TaskCustomAttributesValuesPermission,) lookup_field = "task_id" content_object = "task" @@ -112,6 +118,7 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.IssueCustomAttributesValues serializer_class = serializers.IssueCustomAttributesValuesSerializer + validator_class = validators.IssueCustomAttributesValuesValidator permission_classes = (permissions.IssueCustomAttributesValuesPermission,) lookup_field = "issue_id" content_object = "issue" diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index 64a934f5..d4fc084e 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -17,131 +17,51 @@ # along with this program. If not, see . -from django.apps import apps -from django.utils.translation import ugettext_lazy as _ - -from taiga.base.fields import JsonField -from taiga.base.api.serializers import ValidationError -from taiga.base.api.serializers import ModelSerializer - -from . import models +from taiga.base.fields import JsonField, Field +from taiga.base.api import serializers ###################################################### # Custom Attribute Serializer ####################################################### -class BaseCustomAttributeSerializer(ModelSerializer): - class Meta: - read_only_fields = ('id',) - exclude = ('created_date', 'modified_date') - - def _validate_integrity_between_project_and_name(self, attrs, source): - """ - Check the name is not duplicated in the project. Check when: - - create a new one - - update the name - - update the project (move to another project) - """ - data_id = attrs.get("id", None) - data_name = attrs.get("name", None) - data_project = attrs.get("project", None) - - if self.object: - data_id = data_id or self.object.id - data_name = data_name or self.object.name - data_project = data_project or self.object.project - - model = self.Meta.model - qs = (model.objects.filter(project=data_project, name=data_name) - .exclude(id=data_id)) - if qs.exists(): - raise ValidationError(_("Already exists one with the same name.")) - - return attrs - - def validate_name(self, attrs, source): - return self._validate_integrity_between_project_and_name(attrs, source) - - def validate_project(self, attrs, source): - return self._validate_integrity_between_project_and_name(attrs, source) +class BaseCustomAttributeSerializer(serializers.LightSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + project = Field(attr="project_id") + created_date = Field() + modified_date = Field() class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer): - class Meta(BaseCustomAttributeSerializer.Meta): - model = models.UserStoryCustomAttribute + pass class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer): - class Meta(BaseCustomAttributeSerializer.Meta): - model = models.TaskCustomAttribute + pass class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer): - class Meta(BaseCustomAttributeSerializer.Meta): - model = models.IssueCustomAttribute + pass ###################################################### # Custom Attribute Serializer ####################################################### - - -class BaseCustomAttributesValuesSerializer(ModelSerializer): - attributes_values = JsonField(source="attributes_values", label="attributes values") - _custom_attribute_model = None - _container_field = None - - class Meta: - exclude = ("id",) - - def validate_attributes_values(self, attrs, source): - # values must be a dict - data_values = attrs.get("attributes_values", None) - if self.object: - data_values = (data_values or self.object.attributes_values) - - if type(data_values) is not dict: - raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) - - # Values keys must be in the container object project - data_container = attrs.get(self._container_field, None) - if data_container: - project_id = data_container.project_id - elif self.object: - project_id = getattr(self.object, self._container_field).project_id - else: - project_id = None - - values_ids = list(data_values.keys()) - qs = self._custom_attribute_model.objects.filter(project=project_id, - id__in=values_ids) - if qs.count() != len(values_ids): - raise ValidationError(_("It contain invalid custom fields.")) - - return attrs +class BaseCustomAttributesValuesSerializer(serializers.LightSerializer): + attributes_values = Field() + version = Field() class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): - _custom_attribute_model = models.UserStoryCustomAttribute - _container_model = "userstories.UserStory" - _container_field = "user_story" - - class Meta(BaseCustomAttributesValuesSerializer.Meta): - model = models.UserStoryCustomAttributesValues + user_story = Field(attr="user_story.id") -class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): - _custom_attribute_model = models.TaskCustomAttribute - _container_field = "task" - - class Meta(BaseCustomAttributesValuesSerializer.Meta): - model = models.TaskCustomAttributesValues +class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): + task = Field(attr="task.id") -class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): - _custom_attribute_model = models.IssueCustomAttribute - _container_field = "issue" - - class Meta(BaseCustomAttributesValuesSerializer.Meta): - model = models.IssueCustomAttributesValues +class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): + issue = Field(attr="issue.id") diff --git a/taiga/projects/custom_attributes/validators.py b/taiga/projects/custom_attributes/validators.py new file mode 100644 index 00000000..506c040c --- /dev/null +++ b/taiga/projects/custom_attributes/validators.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.fields import JsonField +from taiga.base.api.serializers import ValidationError +from taiga.base.api.validators import ModelValidator + +from . import models + + +###################################################### +# Custom Attribute Validator +####################################################### + +class BaseCustomAttributeValidator(ModelValidator): + class Meta: + read_only_fields = ('id',) + exclude = ('created_date', 'modified_date') + + def _validate_integrity_between_project_and_name(self, attrs, source): + """ + Check the name is not duplicated in the project. Check when: + - create a new one + - update the name + - update the project (move to another project) + """ + data_id = attrs.get("id", None) + data_name = attrs.get("name", None) + data_project = attrs.get("project", None) + + if self.object: + data_id = data_id or self.object.id + data_name = data_name or self.object.name + data_project = data_project or self.object.project + + model = self.Meta.model + qs = (model.objects.filter(project=data_project, name=data_name) + .exclude(id=data_id)) + if qs.exists(): + raise ValidationError(_("Already exists one with the same name.")) + + return attrs + + def validate_name(self, attrs, source): + return self._validate_integrity_between_project_and_name(attrs, source) + + def validate_project(self, attrs, source): + return self._validate_integrity_between_project_and_name(attrs, source) + + +class UserStoryCustomAttributeValidator(BaseCustomAttributeValidator): + class Meta(BaseCustomAttributeValidator.Meta): + model = models.UserStoryCustomAttribute + + +class TaskCustomAttributeValidator(BaseCustomAttributeValidator): + class Meta(BaseCustomAttributeValidator.Meta): + model = models.TaskCustomAttribute + + +class IssueCustomAttributeValidator(BaseCustomAttributeValidator): + class Meta(BaseCustomAttributeValidator.Meta): + model = models.IssueCustomAttribute + + +###################################################### +# Custom Attribute Validator +####################################################### + + +class BaseCustomAttributesValuesValidator(ModelValidator): + attributes_values = JsonField(source="attributes_values", label="attributes values") + _custom_attribute_model = None + _container_field = None + + class Meta: + exclude = ("id",) + + def validate_attributes_values(self, attrs, source): + # values must be a dict + data_values = attrs.get("attributes_values", None) + if self.object: + data_values = (data_values or self.object.attributes_values) + + if type(data_values) is not dict: + raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) + + # Values keys must be in the container object project + data_container = attrs.get(self._container_field, None) + if data_container: + project_id = data_container.project_id + elif self.object: + project_id = getattr(self.object, self._container_field).project_id + else: + project_id = None + + values_ids = list(data_values.keys()) + qs = self._custom_attribute_model.objects.filter(project=project_id, + id__in=values_ids) + if qs.count() != len(values_ids): + raise ValidationError(_("It contain invalid custom fields.")) + + return attrs + + +class UserStoryCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator): + _custom_attribute_model = models.UserStoryCustomAttribute + _container_model = "userstories.UserStory" + _container_field = "user_story" + + class Meta(BaseCustomAttributesValuesValidator.Meta): + model = models.UserStoryCustomAttributesValues + + +class TaskCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator, ModelValidator): + _custom_attribute_model = models.TaskCustomAttribute + _container_field = "task" + + class Meta(BaseCustomAttributesValuesValidator.Meta): + model = models.TaskCustomAttributesValues + + +class IssueCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator, ModelValidator): + _custom_attribute_model = models.IssueCustomAttribute + _container_field = "issue" + + class Meta(BaseCustomAttributesValuesValidator.Meta): + model = models.IssueCustomAttributesValues From 9c8a630fc6dca8653dc062e2f56e99ffa3597224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 6 Jul 2016 12:46:22 +0200 Subject: [PATCH 095/261] Moving ValidationError to taiga.base.exceptions --- taiga/auth/validators.py | 6 ++--- taiga/base/api/fields.py | 3 ++- taiga/base/api/mixins.py | 2 +- taiga/base/api/relations.py | 3 ++- taiga/base/api/serializers.py | 2 ++ taiga/base/exceptions.py | 5 +++++ taiga/export_import/serializers/fields.py | 2 +- .../export_import/serializers/serializers.py | 22 +++++++++---------- taiga/projects/api.py | 3 +-- .../projects/custom_attributes/validators.py | 2 +- taiga/projects/milestones/validators.py | 4 ++-- taiga/projects/notifications/validators.py | 4 ++-- taiga/projects/references/validators.py | 7 +++--- taiga/projects/tagging/fields.py | 2 +- taiga/projects/tasks/validators.py | 3 ++- taiga/projects/userstories/validators.py | 7 +++--- taiga/projects/validators.py | 17 +++++++------- taiga/users/serializers.py | 6 ----- taiga/users/validators.py | 12 +++++----- taiga/userstorage/api.py | 1 - 20 files changed, 58 insertions(+), 55 deletions(-) diff --git a/taiga/auth/validators.py b/taiga/auth/validators.py index 6c3661ed..a18dc4bc 100644 --- a/taiga/auth/validators.py +++ b/taiga/auth/validators.py @@ -17,11 +17,11 @@ # along with this program. If not, see . from django.core import validators as core_validators -from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.api import validators +from taiga.base.exceptions import ValidationError import re @@ -39,8 +39,8 @@ class BaseRegisterValidator(validators.Validator): try: validator(value) except ValidationError: - raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, numbers " - "and /./-/_ characters'")) + raise ValidationError(_("Required. 255 characters or fewer. Letters, numbers " + "and /./-/_ characters'")) return attrs diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index 7dfa2c0a..fc4035c2 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -50,7 +50,6 @@ 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 @@ -66,6 +65,8 @@ from django.utils.functional import Promise from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ +from taiga.base.exceptions import ValidationError + from . import ISO_8601 from .settings import api_settings diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index 861d77ec..c38b5cb7 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -44,12 +44,12 @@ 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 taiga.base.exceptions import ValidationError from .settings import api_settings from .utils import get_object_or_404 diff --git a/taiga/base/api/relations.py b/taiga/base/api/relations.py index 60ba9a6e..6fbb98f5 100644 --- a/taiga/base/api/relations.py +++ b/taiga/base/api/relations.py @@ -48,7 +48,7 @@ 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.exceptions import ObjectDoesNotExist from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch from django import forms from django.db.models.fields import BLANK_CHOICE_DASH @@ -59,6 +59,7 @@ from django.utils.translation import ugettext_lazy as _ from .fields import Field, WritableField, get_component, is_simple_callable from .reverse import reverse +from taiga.base.exceptions import ValidationError import warnings from urllib import parse as urlparse diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 82565b26..f2dfd849 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -78,6 +78,8 @@ import serpy # This helps keep the separation between model fields, form fields, and # serializer fields more explicit. +from taiga.base.exceptions import ValidationError + from .relations import * from .fields import * diff --git a/taiga/base/exceptions.py b/taiga/base/exceptions.py index cc58ee6d..73d277ff 100644 --- a/taiga/base/exceptions.py +++ b/taiga/base/exceptions.py @@ -51,6 +51,7 @@ In addition Django's built in 403 and 404 exceptions are handled. """ from django.core.exceptions import PermissionDenied as DjangoPermissionDenied +from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from django.http import Http404 @@ -224,6 +225,7 @@ class NotEnoughSlotsForProject(BaseException): "total_memberships": total_memberships } + def format_exception(exc): if isinstance(exc.detail, (dict, list, tuple,)): detail = exc.detail @@ -270,3 +272,6 @@ def exception_handler(exc): # Note: Unhandled exceptions will raise a 500 error. return None + + +ValidationError = DjangoValidationError diff --git a/taiga/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py index 64c01436..9ed21a19 100644 --- a/taiga/export_import/serializers/fields.py +++ b/taiga/export_import/serializers/fields.py @@ -23,11 +23,11 @@ from collections import OrderedDict from django.core.files.base import ContentFile from django.core.exceptions import ObjectDoesNotExist -from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from django.contrib.contenttypes.models import ContentType from taiga.base.api import serializers +from taiga.base.exceptions import ValidationError from taiga.base.fields import JsonField from taiga.mdrender.service import render as mdrender from taiga.users import models as users_models diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py index 7cf46cba..6a316b68 100644 --- a/taiga/export_import/serializers/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -16,13 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import copy - -from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.fields import JsonField, PgArrayField +from taiga.base.exceptions import ValidationError from taiga.projects import models as projects_models from taiga.projects.custom_attributes import models as custom_attributes_models @@ -31,15 +29,12 @@ from taiga.projects.tasks import models as tasks_models from taiga.projects.issues import models as issues_models from taiga.projects.milestones import models as milestones_models from taiga.projects.wiki import models as wiki_models -from taiga.projects.history import models as history_models -from taiga.projects.attachments import models as attachments_models from taiga.timeline import models as timeline_models from taiga.users import models as users_models from taiga.projects.votes import services as votes_service -from .fields import (FileField, RelatedNoneSafeField, UserRelatedField, - UserPkField, CommentField, ProjectRelatedField, - HistoryUserField, HistoryValuesField, HistoryDiffField, +from .fields import (FileField, UserRelatedField, + ProjectRelatedField, TimelineDataField, ContentTypeField) from .mixins import (HistoryExportSerializerMixin, AttachmentExportSerializerMixin, @@ -125,7 +120,7 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): - attributes_values = JsonField(source="attributes_values",required=True) + attributes_values = JsonField(source="attributes_values", required=True) _custom_attribute_model = None _container_field = None @@ -158,6 +153,7 @@ class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): return attrs + class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute _container_model = "userstories.UserStory" @@ -224,7 +220,7 @@ class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin): name = attrs[source] qs = self.project.milestones.filter(name=name) if qs.exists(): - raise serializers.ValidationError(_("Name duplicated for the project")) + raise ValidationError(_("Name duplicated for the project")) return attrs @@ -268,7 +264,9 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His def custom_attributes_queryset(self, project): if project.id not in _custom_userstories_attributes_cache: - _custom_userstories_attributes_cache[project.id] = list(project.userstorycustomattributes.all().values('id', 'name')) + _custom_userstories_attributes_cache[project.id] = list( + project.userstorycustomattributes.all().values('id', 'name') + ) return _custom_userstories_attributes_cache[project.id] @@ -314,10 +312,10 @@ class WikiLinkExportSerializer(serializers.ModelSerializer): exclude = ('id', 'project') - class TimelineExportSerializer(serializers.ModelSerializer): data = TimelineDataField() data_content_type = ContentTypeField() + class Meta: model = timeline_models.Timeline exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') diff --git a/taiga/projects/api.py b/taiga/projects/api.py index c441e419..6445c17f 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -22,7 +22,6 @@ from dateutil.relativedelta import relativedelta from django.apps import apps from django.conf import settings -from django.core.exceptions import ValidationError from django.http import Http404 from django.utils.translation import ugettext as _ from django.utils import timezone @@ -651,7 +650,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): invitation_extra_text=invitation_extra_text, callback=self.post_save, precall=self.pre_save) - except ValidationError as err: + except exc.ValidationError as err: return response.BadRequest(err.message_dict) members_serialized = self.admin_serializer_class(members, many=True) diff --git a/taiga/projects/custom_attributes/validators.py b/taiga/projects/custom_attributes/validators.py index 506c040c..6663de5d 100644 --- a/taiga/projects/custom_attributes/validators.py +++ b/taiga/projects/custom_attributes/validators.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _ from taiga.base.fields import JsonField -from taiga.base.api.serializers import ValidationError +from taiga.base.exceptions import ValidationError from taiga.base.api.validators import ModelValidator from . import models diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py index 8de3174c..b7d4d484 100644 --- a/taiga/projects/milestones/validators.py +++ b/taiga/projects/milestones/validators.py @@ -18,7 +18,7 @@ from django.utils.translation import ugettext as _ -from taiga.base.api import serializers +from taiga.base.exceptions import ValidationError from taiga.base.api import validators from taiga.projects.validators import DuplicatedNameInProjectValidator from taiga.projects.notifications.validators import WatchersValidator @@ -31,7 +31,7 @@ class MilestoneExistsValidator: value = attrs[source] if not models.Milestone.objects.filter(pk=value).exists(): msg = _("There's no milestone with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs diff --git a/taiga/projects/notifications/validators.py b/taiga/projects/notifications/validators.py index 851cc309..40e02083 100644 --- a/taiga/projects/notifications/validators.py +++ b/taiga/projects/notifications/validators.py @@ -18,7 +18,7 @@ from django.utils.translation import ugettext as _ -from taiga.base.api import serializers +from taiga.base.exceptions import ValidationError class WatchersValidator: @@ -45,6 +45,6 @@ class WatchersValidator: existing_watcher_ids = project.get_watchers().values_list("id", flat=True) result = set(users).difference(member_ids).difference(existing_watcher_ids) if result: - raise serializers.ValidationError(_("Watchers contains invalid users")) + raise ValidationError(_("Watchers contains invalid users")) return attrs diff --git a/taiga/projects/references/validators.py b/taiga/projects/references/validators.py index 5fcefee8..85456c4c 100644 --- a/taiga/projects/references/validators.py +++ b/taiga/projects/references/validators.py @@ -18,6 +18,7 @@ from taiga.base.api import serializers from taiga.base.api import validators +from taiga.base.exceptions import ValidationError class ResolverValidator(validators.Validator): @@ -32,10 +33,10 @@ class ResolverValidator(validators.Validator): def validate(self, attrs): if "ref" in attrs: if "us" in attrs: - raise serializers.ValidationError("'us' param is incompatible with 'ref' in the same request") + raise ValidationError("'us' param is incompatible with 'ref' in the same request") if "task" in attrs: - raise serializers.ValidationError("'task' param is incompatible with 'ref' in the same request") + raise ValidationError("'task' param is incompatible with 'ref' in the same request") if "issue" in attrs: - raise serializers.ValidationError("'issue' param is incompatible with 'ref' in the same request") + raise ValidationError("'issue' param is incompatible with 'ref' in the same request") return attrs diff --git a/taiga/projects/tagging/fields.py b/taiga/projects/tagging/fields.py index 24f92f23..47553d8c 100644 --- a/taiga/projects/tagging/fields.py +++ b/taiga/projects/tagging/fields.py @@ -16,11 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.core.exceptions import ValidationError from django.forms import widgets from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers +from taiga.base.exceptions import ValidationError import re diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index 7f71636c..ddb3f33b 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -20,6 +20,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.api import validators +from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField from taiga.projects.milestones.validators import MilestoneExistsValidator from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer @@ -34,7 +35,7 @@ class TaskExistsValidator: value = attrs[source] if not models.Task.objects.filter(pk=value).exists(): msg = _("There's no task with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 4ea0b24a..2d61934f 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -21,6 +21,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.api import validators from taiga.base.api.utils import get_object_or_404 +from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField from taiga.base.fields import PickledObjectField from taiga.projects.milestones.validators import MilestoneExistsValidator @@ -40,7 +41,7 @@ class UserStoryExistsValidator: value = attrs[source] if not models.UserStory.objects.filter(pk=value).exists(): msg = _("There's no user story with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs @@ -105,9 +106,9 @@ class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValida project = get_object_or_404(Project, pk=data["project_id"]) if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): - raise serializers.ValidationError("all the user stories must be from the same project") + raise ValidationError("all the user stories must be from the same project") if project.milestones.filter(id=data["milestone_id"]).count() != 1: - raise serializers.ValidationError("the milestone isn't valid for the project") + raise ValidationError("the milestone isn't valid for the project") return data diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index c8ab21bb..de06c05c 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -21,6 +21,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.api import validators +from taiga.base.exceptions import ValidationError from taiga.base.fields import JsonField from taiga.base.fields import PgArrayField from taiga.users.validators import RoleExistsValidator @@ -49,7 +50,7 @@ class DuplicatedNameInProjectValidator: qs = model.objects.filter(project=attrs["project"], name=attrs[source]) if qs and qs.exists(): - raise serializers.ValidationError(_("Name duplicated for the project")) + raise ValidationError(_("Name duplicated for the project")) return attrs @@ -59,7 +60,7 @@ class ProjectExistsValidator: value = attrs[source] if not models.Project.objects.filter(pk=value).exists(): msg = _("There's no project with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs @@ -68,7 +69,7 @@ class UserStoryStatusExistsValidator: value = attrs[source] if not models.UserStoryStatus.objects.filter(pk=value).exists(): msg = _("There's no user story status with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs @@ -77,7 +78,7 @@ class TaskStatusExistsValidator: value = attrs[source] if not models.TaskStatus.objects.filter(pk=value).exists(): msg = _("There's no task status with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs @@ -152,7 +153,7 @@ class MembershipValidator(validators.ModelValidator): Q(project_id=project.id, email=email)) if qs.count() > 0: - raise serializers.ValidationError(_("Email address is already taken")) + raise ValidationError(_("Email address is already taken")) return attrs @@ -164,7 +165,7 @@ class MembershipValidator(validators.ModelValidator): role = attrs[source] if project.roles.filter(id=role.id).count() == 0: - raise serializers.ValidationError(_("Invalid role for the project")) + raise ValidationError(_("Invalid role for the project")) return attrs @@ -175,10 +176,10 @@ class MembershipValidator(validators.ModelValidator): if (self.object and self.object.user): if self.object.user.id == project.owner_id and not attrs[source]: - raise serializers.ValidationError(_("The project owner must be admin.")) + raise ValidationError(_("The project owner must be admin.")) if not services.project_has_valid_admins(project, exclude_user=self.object.user): - raise serializers.ValidationError( + raise ValidationError( _("At least one user must be an active admin for this project.") ) diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 75daa74e..a28f5ce3 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -17,9 +17,6 @@ # along with this program. If not, see . from django.conf import settings -from django.core import validators -from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers from taiga.base.fields import PgArrayField, Field, MethodField, I18NField @@ -27,14 +24,11 @@ from taiga.base.fields import PgArrayField, Field, MethodField, I18NField from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project -from .models import User, Role from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url from .gravatar import get_gravatar_url from collections import namedtuple -import re - ###################################################### # User diff --git a/taiga/users/validators.py b/taiga/users/validators.py index 11e78efb..f23da47a 100644 --- a/taiga/users/validators.py +++ b/taiga/users/validators.py @@ -17,12 +17,12 @@ # along with this program. If not, see . from django.core import validators as core_validators -from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers from taiga.base.api import validators -from taiga.base.fields import PgArrayField, Field +from taiga.base.exceptions import ValidationError +from taiga.base.fields import PgArrayField from .models import User, Role @@ -34,7 +34,7 @@ class RoleExistsValidator: value = attrs[source] if not Role.objects.filter(pk=value).exists(): msg = _("There's no role with that id") - raise serializers.ValidationError(msg) + raise ValidationError(msg) return attrs @@ -55,13 +55,13 @@ class UserValidator(validators.ModelValidator): try: validator(value) except ValidationError: - raise validators.ValidationError(_("Required. 255 characters or fewer. Letters, " - "numbers and /./-/_ characters'")) + raise ValidationError(_("Required. 255 characters or fewer. Letters, " + "numbers and /./-/_ characters'")) if (self.object and self.object.username != value and User.objects.filter(username=value).exists()): - raise validators.ValidationError(_("Invalid username. Try with a different one.")) + raise ValidationError(_("Invalid username. Try with a different one.")) return attrs diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py index 5b097e71..94c5ea00 100644 --- a/taiga/userstorage/api.py +++ b/taiga/userstorage/api.py @@ -19,7 +19,6 @@ from django.utils.translation import ugettext as _ from taiga.base.api import ModelCrudViewSet -from taiga.base.api.serializers import ValidationError from taiga.base import exceptions as exc from . import models From dd4a1cd9e776278741444d56eb5d2fce3d79a959 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 6 Jul 2016 14:13:09 +0200 Subject: [PATCH 096/261] Improving by_ref endpoints and allowing to use the project slug --- taiga/projects/issues/api.py | 13 +++++++++++-- taiga/projects/tasks/api.py | 13 +++++++++++-- taiga/projects/userstories/api.py | 13 +++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index f204fc13..093b3ad1 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -183,9 +183,18 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W @list_route(methods=["GET"]) def by_ref(self, request): - ref = request.QUERY_PARAMS.get("ref", None) + retrieve_kwargs = { + "ref": request.QUERY_PARAMS.get("ref", None) + } project_id = request.QUERY_PARAMS.get("project", None) - return self.retrieve(request, project_id=project_id, ref=ref) + if project_id is not None: + retrieve_kwargs["project_id"] = project_id + + project_slug = request.QUERY_PARAMS.get("project__slug", None) + if project_slug is not None: + retrieve_kwargs["project__slug"] = project_slug + + return self.retrieve(request, **retrieve_kwargs) @list_route(methods=["GET"]) def filters_data(self, request, *args, **kwargs): diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index bec134c5..3dc2bd32 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -163,9 +163,18 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, @list_route(methods=["GET"]) def by_ref(self, request): - ref = request.QUERY_PARAMS.get("ref", None) + retrieve_kwargs = { + "ref": request.QUERY_PARAMS.get("ref", None) + } project_id = request.QUERY_PARAMS.get("project", None) - return self.retrieve(request, project_id=project_id, ref=ref) + if project_id is not None: + retrieve_kwargs["project_id"] = project_id + + project_slug = request.QUERY_PARAMS.get("project__slug", None) + if project_slug is not None: + retrieve_kwargs["project__slug"] = project_slug + + return self.retrieve(request, **retrieve_kwargs) @list_route(methods=["GET"]) def csv(self, request): diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 47487e88..0e718d10 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -224,9 +224,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi @list_route(methods=["GET"]) def by_ref(self, request): - ref = request.QUERY_PARAMS.get("ref", None) + retrieve_kwargs = { + "ref": request.QUERY_PARAMS.get("ref", None) + } project_id = request.QUERY_PARAMS.get("project", None) - return self.retrieve(request, project_id=project_id, ref=ref) + if project_id is not None: + retrieve_kwargs["project_id"] = project_id + + project_slug = request.QUERY_PARAMS.get("project__slug", None) + if project_slug is not None: + retrieve_kwargs["project__slug"] = project_slug + + return self.retrieve(request, **retrieve_kwargs) @list_route(methods=["GET"]) def csv(self, request): From 8896d651364cedcff29f2b28f5f182f63133b2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 7 Jul 2016 11:05:39 +0200 Subject: [PATCH 097/261] Fix project members photo --- taiga/base/api/serializers.py | 12 +++++++++++ taiga/projects/history/serializers.py | 4 ++-- taiga/projects/serializers.py | 31 +++++++++++++++++---------- taiga/timeline/serializers.py | 4 ++-- taiga/users/serializers.py | 8 +++---- taiga/users/services.py | 11 +++++++++- taiga/webhooks/serializers.py | 4 ++-- 7 files changed, 52 insertions(+), 22 deletions(-) diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index f2dfd849..2ee05db8 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -1235,3 +1235,15 @@ class LightSerializer(serpy.Serializer): super().__init__(*args, **kwargs) self.context = context self.view = view + + +class LightDictSerializer(serpy.DictSerializer): + def __init__(self, *args, **kwargs): + kwargs.pop("read_only", None) + kwargs.pop("partial", None) + kwargs.pop("files", None) + context = kwargs.pop("context", {}) + view = kwargs.pop("view", {}) + super().__init__(*args, **kwargs) + self.context = context + self.view = view diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index 8407810f..224cc6df 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -19,7 +19,7 @@ from taiga.base.api import serializers from taiga.base.fields import I18NJsonField, Field, MethodField -from taiga.users.services import get_photo_or_gravatar_url +from taiga.users.services import get_user_photo_or_gravatar_url HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type") @@ -46,7 +46,7 @@ class HistoryEntrySerializer(serializers.LightSerializer): def get_user(self, entry): user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} user.update(entry.user) - user["photo"] = get_photo_or_gravatar_url(entry.owner) + user["photo"] = get_user_photo_or_gravatar_url(entry.owner) if entry.owner: user["is_active"] = entry.owner.is_active diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 96b8d0ba..d4c93155 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -22,7 +22,7 @@ from taiga.base.api import serializers from taiga.base.fields import Field, MethodField, I18NField from taiga.permissions import services as permissions_services -from taiga.users.services import get_photo_or_gravatar_url +from taiga.users.services import get_user_photo_or_gravatar_url, get_photo_or_gravatar_url from taiga.users.serializers import UserBasicInfoSerializer from taiga.permissions.services import calculate_permissions @@ -97,6 +97,23 @@ class IssueTypeSerializer(serializers.LightSerializer): # Members ###################################################### +class MembershipDictSerializer(serializers.LightDictSerializer): + role_name = Field() + full_name = Field() + full_name_display = MethodField() + is_active = Field() + id = Field() + color = Field() + username = Field() + photo = MethodField() + + def get_full_name_display(self, obj): + return obj["full_name"] or obj["username"] or obj["email"] + + def get_photo(self, obj): + return get_photo_or_gravatar_url(obj['photo'], obj['email']) + + class MembershipSerializer(serializers.LightSerializer): id = Field() user = Field(attr="user_id") @@ -130,7 +147,7 @@ class MembershipSerializer(serializers.LightSerializer): return obj.user.color if obj.user else None def get_photo(self, obj): - return get_photo_or_gravatar_url(obj.user) + return get_user_photo_or_gravatar_url(obj.user) def get_project_name(self, obj): return obj.project.name if obj and obj.project else "" @@ -369,15 +386,7 @@ class ProjectDetailSerializer(ProjectSerializer): if obj.members_attr is None: return [] - ret = [] - for m in obj.members_attr: - m["full_name_display"] = m["full_name"] or m["username"] or m["email"] - del(m["email"]) - del(m["complete_user_name"]) - if not m["id"] is None: - ret.append(m) - - return ret + return MembershipDictSerializer([m for m in obj.members_attr if m['id'] is not None], many=True).data def get_total_memberships(self, obj): if obj.members_attr is None: diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index 07b1985a..a7f607c9 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -20,7 +20,7 @@ from django.contrib.auth import get_user_model from taiga.base.api import serializers from taiga.base.fields import Field, MethodField -from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url +from taiga.users.services import get_user_photo_or_gravatar_url, get_big_photo_or_gravatar_url from . import models @@ -56,7 +56,7 @@ class TimelineSerializer(serializers.LightSerializer): obj.data["user"] = { "id": user.pk, "name": user.get_full_name(), - "photo": get_photo_or_gravatar_url(user), + "photo": get_user_photo_or_gravatar_url(user), "big_photo": get_big_photo_or_gravatar_url(user), "username": user.username, "is_profile_visible": user.is_active and not user.is_system, diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index a28f5ce3..a943183d 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -24,7 +24,7 @@ from taiga.base.fields import PgArrayField, Field, MethodField, I18NField from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project -from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url +from .services import get_user_photo_or_gravatar_url, get_big_photo_or_gravatar_url from .gravatar import get_gravatar_url from collections import namedtuple @@ -61,7 +61,7 @@ class UserSerializer(serializers.LightSerializer): return obj.get_full_name() if obj else "" def get_photo(self, user): - return get_photo_or_gravatar_url(user) + return get_user_photo_or_gravatar_url(user) def get_big_photo(self, user): return get_big_photo_or_gravatar_url(user) @@ -115,7 +115,7 @@ class UserBasicInfoSerializer(serializers.LightSerializer): return obj.get_full_name() def get_photo(self, obj): - return get_photo_or_gravatar_url(obj) + return get_user_photo_or_gravatar_url(obj) def get_big_photo(self, obj): return get_big_photo_or_gravatar_url(obj) @@ -237,7 +237,7 @@ class HighLightedContentSerializer(serializers.LightSerializer): UserData = namedtuple("UserData", ["photo", "email"]) user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "") - return get_photo_or_gravatar_url(user_data) + return get_user_photo_or_gravatar_url(user_data) def get_tags_colors(self, obj): tags = getattr(obj, "tags", []) diff --git a/taiga/users/services.py b/taiga/users/services.py index 5397c46b..73ae2d74 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -82,7 +82,16 @@ def get_photo_url(photo): return None -def get_photo_or_gravatar_url(user): +def get_photo_or_gravatar_url(photo=None, email=None): + """Get the user's photo/gravatar url.""" + if photo: + return get_photo_url(photo) + if email: + return get_gravatar_url(email) + return settings.GRAVATAR_DEFAULT_AVATAR + + +def get_user_photo_or_gravatar_url(user): """Get the user's photo/gravatar url.""" if user: return get_photo_url(user.photo) if user.photo else get_gravatar_url(user.email) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index 624e107c..f08f4ae7 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -25,7 +25,7 @@ from taiga.front.templatetags.functions import resolve as resolve_front_url from taiga.projects.services import get_logo_big_thumbnail_url from taiga.users.gravatar import get_gravatar_url -from taiga.users.services import get_photo_or_gravatar_url +from taiga.users.services import get_user_photo_or_gravatar_url ######################################################################## @@ -82,7 +82,7 @@ class UserSerializer(serializers.LightSerializer): return obj.get_full_name() def get_photo(self, obj): - return get_photo_or_gravatar_url(obj) + return get_user_photo_or_gravatar_url(obj) def to_value(self, instance): if instance is None: From c3022f4f74d91f2e455db17173fcc9fd29964400 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 7 Jul 2016 13:09:03 +0200 Subject: [PATCH 098/261] Fixing reduce_dim sql function --- .../projects/migrations/0046_triggers_to_update_tags_colors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py index 28296036..abb4806d 100644 --- a/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py +++ b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py @@ -21,6 +21,9 @@ class Migration(migrations.Migration): DECLARE s $1%TYPE; BEGIN + IF $1 = '{}' THEN + RETURN; + END IF; FOREACH s SLICE 1 IN ARRAY $1 LOOP RETURN NEXT s; END LOOP; From 20a7595ca3b7344a2affa26792ae5cf1793f5616 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 7 Jul 2016 13:57:07 +0200 Subject: [PATCH 099/261] Including the gravatar id in the responses and not generating the gravatar avatar url by default --- settings/common.py | 4 --- taiga/projects/history/serializers.py | 6 ++-- taiga/projects/serializers.py | 15 +++++++-- taiga/timeline/serializers.py | 8 +++-- taiga/users/gravatar.py | 45 +++++++-------------------- taiga/users/serializers.py | 34 ++++++++++++++------ taiga/users/services.py | 38 ++++++++-------------- taiga/webhooks/serializers.py | 16 +++++----- tests/unit/test_gravatar.py | 31 ------------------ 9 files changed, 77 insertions(+), 120 deletions(-) delete mode 100644 tests/unit/test_gravatar.py diff --git a/settings/common.py b/settings/common.py index f227d97c..3ee3b3f3 100644 --- a/settings/common.py +++ b/settings/common.py @@ -477,10 +477,6 @@ THUMBNAIL_ALIASES = { }, } -# GRAVATAR_DEFAULT_AVATAR = "img/user-noimage.png" -GRAVATAR_DEFAULT_AVATAR = "" -GRAVATAR_AVATAR_SIZE = THN_AVATAR_SIZE - TAGS_PREDEFINED_COLORS = ["#fce94f", "#edd400", "#c4a000", "#8ae234", "#73d216", "#4e9a06", "#d3d7cf", "#fcaf3e", "#f57900", "#ce5c00", "#729fcf", "#3465a4", diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index 224cc6df..0f2dc658 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -19,7 +19,8 @@ from taiga.base.api import serializers from taiga.base.fields import I18NJsonField, Field, MethodField -from taiga.users.services import get_user_photo_or_gravatar_url +from taiga.users.services import get_user_photo_url +from taiga.users.gravatar import get_user_gravatar_id HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type") @@ -46,7 +47,8 @@ class HistoryEntrySerializer(serializers.LightSerializer): def get_user(self, entry): user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} user.update(entry.user) - user["photo"] = get_user_photo_or_gravatar_url(entry.owner) + user["photo"] = get_user_photo_url(entry.owner) + user["gravatar_id"] = get_user_gravatar_id(entry.owner) if entry.owner: user["is_active"] = entry.owner.is_active diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index d4c93155..1d8a5799 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -22,7 +22,8 @@ from taiga.base.api import serializers from taiga.base.fields import Field, MethodField, I18NField from taiga.permissions import services as permissions_services -from taiga.users.services import get_user_photo_or_gravatar_url, get_photo_or_gravatar_url +from taiga.users.services import get_photo_url, get_user_photo_url +from taiga.users.gravatar import get_gravatar_id, get_user_gravatar_id from taiga.users.serializers import UserBasicInfoSerializer from taiga.permissions.services import calculate_permissions @@ -106,12 +107,16 @@ class MembershipDictSerializer(serializers.LightDictSerializer): color = Field() username = Field() photo = MethodField() + gravatar_id = MethodField() def get_full_name_display(self, obj): return obj["full_name"] or obj["username"] or obj["email"] def get_photo(self, obj): - return get_photo_or_gravatar_url(obj['photo'], obj['email']) + return get_photo_url(obj['photo']) + + def get_gravatar_id(self, obj): + return get_gravatar_id(obj['email']) class MembershipSerializer(serializers.LightSerializer): @@ -129,6 +134,7 @@ class MembershipSerializer(serializers.LightSerializer): is_user_active = MethodField() color = MethodField() photo = MethodField() + gravatar_id = MethodField() project_name = MethodField() project_slug = MethodField() invited_by = UserBasicInfoSerializer() @@ -147,7 +153,10 @@ class MembershipSerializer(serializers.LightSerializer): return obj.user.color if obj.user else None def get_photo(self, obj): - return get_user_photo_or_gravatar_url(obj.user) + return get_user_photo_url(obj.user) + + def get_gravatar_id(self, obj): + return get_user_gravatar_id(obj.user) def get_project_name(self, obj): return obj.project.name if obj and obj.project else "" diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index a7f607c9..0b831d04 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -20,7 +20,8 @@ from django.contrib.auth import get_user_model from taiga.base.api import serializers from taiga.base.fields import Field, MethodField -from taiga.users.services import get_user_photo_or_gravatar_url, get_big_photo_or_gravatar_url +from taiga.users.services import get_user_photo_url, get_user_big_photo_url +from taiga.users.gravatar import get_user_gravatar_id from . import models @@ -56,8 +57,9 @@ class TimelineSerializer(serializers.LightSerializer): obj.data["user"] = { "id": user.pk, "name": user.get_full_name(), - "photo": get_user_photo_or_gravatar_url(user), - "big_photo": get_big_photo_or_gravatar_url(user), + "photo": get_user_photo_url(user), + "big_photo": get_user_big_photo_url(user), + "gravatar_id": get_user_gravatar_id(user), "username": user.username, "is_profile_visible": user.is_active and not user.is_system, "date_joined": user.date_joined diff --git a/taiga/users/gravatar.py b/taiga/users/gravatar.py index 7793e59d..b8329d95 100644 --- a/taiga/users/gravatar.py +++ b/taiga/users/gravatar.py @@ -18,45 +18,22 @@ # along with this program. If not, see . import hashlib -import copy - -from urllib.parse import urlencode - -from django.conf import settings -from django.templatetags.static import static - -GRAVATAR_BASE_URL = "//www.gravatar.com/avatar/{}?{}" -def get_gravatar_url(email: str, **options) -> str: - """Get the gravatar url associated to an email. +def get_gravatar_id(email: str) -> str: + """Get the gravatar id associated to an email. - :param options: Additional options to gravatar. - - `default` defines what image url to show if no gravatar exists - - `size` defines the size of the avatar. - - :return: Gravatar url. + :return: Gravatar id. """ - params = copy.copy(options) + return hashlib.md5(email.lower().encode()).hexdigest() - default_avatar = getattr(settings, "GRAVATAR_DEFAULT_AVATAR", None) - default_size = getattr(settings, "GRAVATAR_AVATAR_SIZE", None) +def get_user_gravatar_id(user: object) -> str: + """Get the gravatar id associated to a user. - avatar = options.get("default", None) - size = options.get("size", None) + :return: Gravatar id. + """ + if user and user.email: + return get_gravatar_id(user.email) - if avatar: - params["default"] = avatar - elif default_avatar: - params["default"] = static(default_avatar) - - if size: - params["size"] = size - elif default_size: - params["size"] = default_size - - email_hash = hashlib.md5(email.lower().encode()).hexdigest() - url = GRAVATAR_BASE_URL.format(email_hash, urlencode(params)) - - return url + return None diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index a943183d..76fa6141 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -24,8 +24,8 @@ from taiga.base.fields import PgArrayField, Field, MethodField, I18NField from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project -from .services import get_user_photo_or_gravatar_url, get_big_photo_or_gravatar_url -from .gravatar import get_gravatar_url +from .services import get_user_photo_url, get_big_photo_url, get_user_big_photo_url +from taiga.users.gravatar import get_user_gravatar_id from collections import namedtuple @@ -53,7 +53,7 @@ class UserSerializer(serializers.LightSerializer): is_active = Field() photo = MethodField() big_photo = MethodField() - gravatar_url = MethodField() + gravatar_id = MethodField() roles = MethodField() projects_with_me = MethodField() @@ -61,13 +61,13 @@ class UserSerializer(serializers.LightSerializer): return obj.get_full_name() if obj else "" def get_photo(self, user): - return get_user_photo_or_gravatar_url(user) + return get_user_photo_url(user) def get_big_photo(self, user): - return get_big_photo_or_gravatar_url(user) + return get_user_big_photo_url(user) - def get_gravatar_url(self, user): - return get_gravatar_url(user.email) + def get_gravatar_id(self, user): + return get_user_gravatar_id(user) def get_roles(self, user): return user.memberships. order_by("role__name").values_list("role__name", flat=True).distinct() @@ -108,6 +108,7 @@ class UserBasicInfoSerializer(serializers.LightSerializer): full_name_display = MethodField() photo = MethodField() big_photo = MethodField() + gravatar_id = MethodField() is_active = Field() id = Field() @@ -115,10 +116,13 @@ class UserBasicInfoSerializer(serializers.LightSerializer): return obj.get_full_name() def get_photo(self, obj): - return get_user_photo_or_gravatar_url(obj) + return get_user_photo_url(obj) def get_big_photo(self, obj): - return get_big_photo_or_gravatar_url(obj) + return get_user_big_photo_url(obj) + + def get_gravatar_id(self, obj): + return get_user_gravatar_id(obj) def to_value(self, instance): if instance is None: @@ -181,6 +185,7 @@ class HighLightedContentSerializer(serializers.LightSerializer): assigned_to_username = Field() assigned_to_full_name = Field() assigned_to_photo = MethodField() + assigned_to_gravatar_id = MethodField() is_watcher = MethodField() total_watchers = Field() @@ -237,7 +242,16 @@ class HighLightedContentSerializer(serializers.LightSerializer): UserData = namedtuple("UserData", ["photo", "email"]) user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "") - return get_user_photo_or_gravatar_url(user_data) + return get_user_photo_url(user_data) + + def get_assigned_to_gravatar_id(self, obj): + type = getattr(obj, "type", "") + if type == "project": + return None + + UserData = namedtuple("UserData", ["photo", "email"]) + user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "") + return get_user_gravatar_id(user_data) def get_tags_colors(self, obj): tags = getattr(obj, "tags", []) diff --git a/taiga/users/services.py b/taiga/users/services.py index 73ae2d74..98b813b1 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -37,9 +37,6 @@ from taiga.base.utils.urls import get_absolute_url from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.services import get_projects_watched -from .gravatar import get_gravatar_url - - def get_user_by_username_or_email(username_or_email): user_model = get_user_model() @@ -75,6 +72,8 @@ def get_and_validate_user(*, username:str, password:str) -> bool: def get_photo_url(photo): """Get a photo absolute url and the photo automatically cropped.""" + if not photo: + return None try: url = get_thumbnailer(photo)[settings.THN_AVATAR_SMALL].url return get_absolute_url(url) @@ -82,24 +81,17 @@ def get_photo_url(photo): return None -def get_photo_or_gravatar_url(photo=None, email=None): - """Get the user's photo/gravatar url.""" - if photo: - return get_photo_url(photo) - if email: - return get_gravatar_url(email) - return settings.GRAVATAR_DEFAULT_AVATAR - - -def get_user_photo_or_gravatar_url(user): - """Get the user's photo/gravatar url.""" - if user: - return get_photo_url(user.photo) if user.photo else get_gravatar_url(user.email) - return settings.GRAVATAR_DEFAULT_AVATAR +def get_user_photo_url(user): + """Get the user's photo url.""" + if not user: + return None + return get_photo_url(user.photo) def get_big_photo_url(photo): """Get a big photo absolute url and the photo automatically cropped.""" + if not photo: + return None try: url = get_thumbnailer(photo)[settings.THN_AVATAR_BIG].url return get_absolute_url(url) @@ -107,15 +99,11 @@ def get_big_photo_url(photo): return None -def get_big_photo_or_gravatar_url(user): - """Get the user's big photo/gravatar url.""" +def get_user_big_photo_url(user): + """Get the user's big photo url.""" if not user: - return "" - - if user.photo: - return get_big_photo_url(user.photo) - else: - return get_gravatar_url(user.email, size=settings.THN_AVATAR_BIG_SIZE) + return None + return get_big_photo_url(user.photo) def get_visible_project_ids(from_user, by_user): diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index f08f4ae7..61eea6cb 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -24,14 +24,14 @@ from taiga.front.templatetags.functions import resolve as resolve_front_url from taiga.projects.services import get_logo_big_thumbnail_url -from taiga.users.gravatar import get_gravatar_url -from taiga.users.services import get_user_photo_or_gravatar_url - +from taiga.users.services import get_user_photo_url +from taiga.users.gravatar import get_user_gravatar_id ######################################################################## # WebHooks ######################################################################## + class WebhookSerializer(serializers.LightSerializer): id = Field() project = Field(attr="project_id") @@ -64,17 +64,14 @@ class WebhookLogSerializer(serializers.LightSerializer): class UserSerializer(serializers.LightSerializer): id = Field(attr="pk") permalink = MethodField() - gravatar_url = MethodField() username = MethodField() full_name = MethodField() photo = MethodField() + gravatar_id = MethodField() def get_permalink(self, obj): return resolve_front_url("user", obj.username) - def get_gravatar_url(self, obj): - return get_gravatar_url(obj.email) - def get_username(self, obj): return obj.get_username() @@ -82,7 +79,10 @@ class UserSerializer(serializers.LightSerializer): return obj.get_full_name() def get_photo(self, obj): - return get_user_photo_or_gravatar_url(obj) + return get_user_photo_url(obj) + + def get_gravatar_id(self, obj): + return get_user_gravatar_id(obj) def to_value(self, instance): if instance is None: diff --git a/tests/unit/test_gravatar.py b/tests/unit/test_gravatar.py deleted file mode 100644 index b6246fa8..00000000 --- a/tests/unit/test_gravatar.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino -# Copyright (C) 2014-2016 David Barragán -# Copyright (C) 2014-2016 Alejandro Alonso -# 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 . - -import hashlib - -from taiga.users.gravatar import get_gravatar_url - - -def test_get_gravatar_url(): - email = "user@email.com" - email_hash = hashlib.md5(email.encode()).hexdigest() - url = get_gravatar_url(email, s=40, d="default-image-url") - - assert email_hash in url - assert 's=40' in url - assert 'd=default-image-url' in url From c110d6d1a2a0546bd3ce6e2831d03405ef7156d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 29 Jun 2016 20:55:06 +0200 Subject: [PATCH 100/261] Remove storage.path usage for allow other Storage Backends --- taiga/export_import/api.py | 6 ++---- taiga/export_import/tasks.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index eaff499d..da2af132 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -76,13 +76,11 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet) if dump_format == "gzip": path = "exports/{}/{}-{}.json.gz".format(project.pk, project.slug, uuid.uuid4().hex) - storage_path = default_storage.path(path) - with default_storage.open(storage_path, mode="wb") as outfile: + with default_storage.open(path, mode="wb") as outfile: services.render_project(project, gzip.GzipFile(fileobj=outfile)) else: path = "exports/{}/{}-{}.json".format(project.pk, project.slug, uuid.uuid4().hex) - storage_path = default_storage.path(path) - with default_storage.open(storage_path, mode="wb") as outfile: + with default_storage.open(path, mode="wb") as outfile: services.render_project(project, outfile) response_data = { diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py index aa75c257..5acb08a2 100644 --- a/taiga/export_import/tasks.py +++ b/taiga/export_import/tasks.py @@ -46,13 +46,11 @@ def dump_project(self, user, project, dump_format): try: if dump_format == "gzip": path = "exports/{}/{}-{}.json.gz".format(project.pk, project.slug, self.request.id) - storage_path = default_storage.path(path) - with default_storage.open(storage_path, mode="wb") as outfile: + with default_storage.open(path, mode="wb") as outfile: services.render_project(project, gzip.GzipFile(fileobj=outfile)) else: path = "exports/{}/{}-{}.json".format(project.pk, project.slug, self.request.id) - storage_path = default_storage.path(path) - with default_storage.open(storage_path, mode="wb") as outfile: + with default_storage.open(path, mode="wb") as outfile: services.render_project(project, outfile) url = default_storage.url(path) From e66b96376fa6ad640a82a40573e14c24c1f8d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 9 Sep 2015 09:15:34 +0200 Subject: [PATCH 101/261] Add filter by email domain on register --- CHANGELOG.md | 1 + settings/common.py | 2 + settings/local.py.example | 4 + taiga/base/api/fields.py | 10 ++- taiga/users/api.py | 2 + tests/integration/test_auth_api.py | 14 ++++ tests/integration/test_memberships.py | 103 ++++++++++++++++++++++++++ tests/integration/test_users.py | 29 ++++++++ 8 files changed, 164 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293cb3d0..1b601d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Import/Export: - Gzip export/import support. - Export performance improvements. +- Add filter by email domain registration and invitation by setting. ### Misc - [API] Improve performance of some calls over list. diff --git a/settings/common.py b/settings/common.py index 3ee3b3f3..66fbb67f 100644 --- a/settings/common.py +++ b/settings/common.py @@ -441,6 +441,8 @@ APP_EXTRA_EXPOSE_HEADERS = [ DEFAULT_PROJECT_TEMPLATE = "scrum" PUBLIC_REGISTER_ENABLED = False +# None or [] values in USER_EMAIL_ALLOWED_DOMAINS means allow any domain +USER_EMAIL_ALLOWED_DOMAINS = None SEARCHES_MAX_RESULTS = 150 diff --git a/settings/local.py.example b/settings/local.py.example index 4ae5a8ab..7e0c44a8 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -105,3 +105,7 @@ DATABASES = { # To use celery in memory #CELERY_ENABLED = True #CELERY_ALWAYS_EAGER = True + +# LIMIT ALLOWED DOMAINS FOR REGISTER AND INVITE +# None or [] values in USER_EMAIL_ALLOWED_DOMAINS means allow any domain +# USER_EMAIL_ALLOWED_DOMAINS = None diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index fc4035c2..c8f55a60 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -612,6 +612,14 @@ class ChoiceField(WritableField): return value +def validate_user_email_allowed_domains(value): + domain_name = value.split("@")[1] + print("EMAIL VALIDATE DOMAIN") + + if settings.USER_EMAIL_ALLOWED_DOMAINS and domain_name not in settings.USER_EMAIL_ALLOWED_DOMAINS: + raise ValidationError(_("You email domain is not allowed")) + + class EmailField(CharField): type_name = "EmailField" type_label = "email" @@ -620,7 +628,7 @@ class EmailField(CharField): default_error_messages = { "invalid": _("Enter a valid email address."), } - default_validators = [validators.validate_email] + default_validators = [validators.validate_email, validate_user_email_allowed_domains] def from_native(self, value): ret = super(EmailField, self).from_native(value) diff --git a/taiga/users/api.py b/taiga/users/api.py index 00d5d279..a9f1c957 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -33,6 +33,7 @@ from taiga.base.decorators import list_route from taiga.base.decorators import detail_route from taiga.base.api import ModelCrudViewSet from taiga.base.api.mixins import BlockedByProjectMixin +from taiga.base.api.fields import validate_user_email_allowed_domains from taiga.base.api.utils import get_object_or_404 from taiga.base.filters import MembersFilterBackend from taiga.base.mails import mail_builder @@ -112,6 +113,7 @@ class UsersViewSet(ModelCrudViewSet): try: validate_email(new_email) + validate_user_email_allowed_domains(new_email) except ValidationError: valid_new_email = False diff --git a/tests/integration/test_auth_api.py b/tests/integration/test_auth_api.py index 5781c58a..0504881b 100644 --- a/tests/integration/test_auth_api.py +++ b/tests/integration/test_auth_api.py @@ -48,6 +48,20 @@ def test_respond_400_when_public_registration_is_disabled(client, register_form, assert response.status_code == 400 +def test_respond_400_when_the_email_domain_isnt_in_allowed_domains(client, register_form, settings): + settings.PUBLIC_REGISTER_ENABLED = True + settings.USER_EMAIL_ALLOWED_DOMAINS = ['other-domain.com'] + response = client.post(reverse("auth-register"), register_form) + assert response.status_code == 400 + + +def test_respond_201_when_the_email_domain_is_in_allowed_domains(client, settings, register_form): + settings.PUBLIC_REGISTER_ENABLED = True + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + response = client.post(reverse("auth-register"), register_form) + assert response.status_code == 201 + + def test_respond_201_with_invitation_without_public_registration(client, register_form, settings): settings.PUBLIC_REGISTER_ENABLED = False user = factories.UserFactory() diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index d5a8a5c3..c6b2ca5e 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -72,6 +72,79 @@ def test_api_create_bulk_members(client): assert response.data[1]["email"] == joseph.email +def test_api_create_bulk_members_with_allowed_domain(client): + project = f.ProjectFactory() + john = f.UserFactory.create() + joseph = f.UserFactory.create() + tester = f.RoleFactory(project=project, name="Tester") + gamer = f.RoleFactory(project=project, name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": "test1@email.com"}, + {"role_id": gamer.pk, "email": "test2@email.com"}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert response.data[0]["email"] == "test1@email.com" + assert response.data[1]["email"] == "test2@email.com" + + +def test_api_create_bulk_members_with_allowed_and_unallowed_domain(client, settings): + project = f.ProjectFactory() + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + tester = f.RoleFactory(project=project, name="Tester") + gamer = f.RoleFactory(project=project, name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": "test@invalid-domain.com"}, + {"role_id": gamer.pk, "email": "test@email.com"}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "email" in response.data["bulk_memberships"][0] + assert "email" not in response.data["bulk_memberships"][1] + + +def test_api_create_bulk_members_with_unallowed_domains(client, settings): + project = f.ProjectFactory() + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + tester = f.RoleFactory(project=project, name="Tester") + gamer = f.RoleFactory(project=project, name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": "test1@invalid-domain.com"}, + {"role_id": gamer.pk, "email": "test2@invalid-domain.com"}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "email" in response.data["bulk_memberships"][0] + assert "email" in response.data["bulk_memberships"][1] + + def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client): user = f.UserFactory.create(max_memberships_private_projects=3) project = f.ProjectFactory(owner=user, is_private=True) @@ -314,6 +387,36 @@ def test_api_create_membership(client): assert response.data["user_email"] == user.email +def test_api_create_membership_with_unallowed_domain(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + + membership = f.MembershipFactory(is_admin=True) + role = f.RoleFactory.create(project=membership.project) + + client.login(membership.user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": role.project.pk, "email": "test@invalid-email.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "email" in response.data + + +def test_api_create_membership_with_allowed_domain(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + + membership = f.MembershipFactory(is_admin=True) + role = f.RoleFactory.create(project=membership.project) + + client.login(membership.user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": role.project.pk, "email": "test@email.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + assert response.data["email"] == "test@email.com" + + def test_api_create_membership_without_enough_memberships_private_project_slots_one_projects(client): user = f.UserFactory.create(max_memberships_private_projects=1) project = f.ProjectFactory(owner=user, is_private=True) diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index f4bf2b51..79f78d11 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -99,6 +99,35 @@ def test_update_user_with_invalid_email(client): assert response.data['_error_message'] == 'Not valid email' +def test_update_user_with_unallowed_domain_email(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + user = f.UserFactory.create(email="my@email.com") + url = reverse('users-detail', kwargs={"pk": user.pk}) + data = {"email": "my@invalid-email.com"} + + client.login(user) + response = client.patch(url, json.dumps(data), content_type="application/json") + + assert response.status_code == 400 + assert response.data['_error_message'] == 'Not valid email' + + +def test_update_user_with_allowed_domain_email(client, settings): + settings.USER_EMAIL_ALLOWED_DOMAINS = ['email.com'] + + user = f.UserFactory.create(email="old@email.com") + url = reverse('users-detail', kwargs={"pk": user.pk}) + data = {"email": "new@email.com"} + + client.login(user) + response = client.patch(url, json.dumps(data), content_type="application/json") + + assert response.status_code == 200 + user = models.User.objects.get(pk=user.id) + assert user.email_token is not None + assert user.new_email == "new@email.com" + + def test_update_user_with_valid_email(client): user = f.UserFactory.create(email="old@email.com") url = reverse('users-detail', kwargs={"pk": user.pk}) From 4257f6d5a0cf10d8f12470db8859d693238b6b9d Mon Sep 17 00:00:00 2001 From: Riccardo Coccioli Date: Tue, 9 Feb 2016 14:05:15 +0100 Subject: [PATCH 102/261] Search also into tags fields Changed the search queries to search also into the tags fields. Fixes #276 --- AUTHORS.rst | 4 +--- taiga/searches/services.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 5be3cfd6..c7aff636 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -20,10 +20,7 @@ answer newbie questions, and generally made taiga that much better: - Andrea Stagi - Andrés Moya - Andrey Alekseenko -<<<<<<< HEAD -======= - Brett Profitt ->>>>>>> master - Bruno Clermont - Chris Wilson - David Burke @@ -33,6 +30,7 @@ answer newbie questions, and generally made taiga that much better: - Julien Palard - luyikei - Motius GmbH +- Riccardo Coccioli - Ricky Posner - Yamila Moreno - Yaser Alraddadi diff --git a/taiga/searches/services.py b/taiga/searches/services.py index 4dcda86f..5e04518a 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -28,6 +28,7 @@ def search_user_stories(project, text): model_cls = apps.get_model("userstories", "UserStory") where_clause = ("to_tsvector('english_nostop', coalesce(userstories_userstory.subject) || ' ' || " "coalesce(userstories_userstory.ref) || ' ' || " + "coalesce(array_to_string(userstories_userstory.tags, ' '), '') || ' ' || " "coalesce(userstories_userstory.description, '')) " "@@ to_tsquery('english_nostop', %s)") @@ -44,6 +45,7 @@ def search_tasks(project, text): model_cls = apps.get_model("tasks", "Task") where_clause = ("to_tsvector('english_nostop', coalesce(tasks_task.subject, '') || ' ' || " "coalesce(tasks_task.ref) || ' ' || " + "coalesce(array_to_string(tasks_task.tags, ' '), '') || ' ' || " "coalesce(tasks_task.description, '')) @@ to_tsquery('english_nostop', %s)") if text: @@ -57,6 +59,7 @@ def search_issues(project, text): model_cls = apps.get_model("issues", "Issue") where_clause = ("to_tsvector('english_nostop', coalesce(issues_issue.subject) || ' ' || " "coalesce(issues_issue.ref) || ' ' || " + "coalesce(array_to_string(issues_issue.tags, ' '), '') || ' ' || " "coalesce(issues_issue.description, '')) @@ to_tsquery('english_nostop', %s)") if text: From aad40e4ba093bd53d444da8c8db52a057579b921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 8 Jul 2016 11:59:38 +0200 Subject: [PATCH 103/261] Add to tags to base filters --- taiga/base/filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index d9369cbd..4aa8cb09 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -459,6 +459,7 @@ class QFilter(FilterBackend): where_clause = (""" to_tsvector('english_nostop', coalesce({table}.subject, '') || ' ' || + coalesce(array_to_string({table}.tags, ' '), '') || ' ' || coalesce({table}.ref) || ' ' || coalesce({table}.description, '')) @@ to_tsquery('english_nostop', %s) """.format(table=table)) From 0b8eabef154d618bc8e2028fb901eab5f0b1d9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 8 Jul 2016 13:04:09 +0200 Subject: [PATCH 104/261] Adding weight to searches --- CHANGELOG.md | 11 +++-- taiga/searches/services.py | 93 ++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b601d3c..be370f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - ProjectTemplates now are sorted by the attribute 'order'. - Create enpty wiki pages (if not exist) when a new link is created. - Diff messages in history entries now show only the relevant changes (with some context). +- Include created, modified and finished dates for tasks in CSV reports +- User stories and tasks listing API call support extra params to include more data (tasks and attachemnts and attachments, respectively) - Comments: - Now comment owners and project admins can edit existing comments with the history Entry endpoint. - Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. @@ -15,10 +17,9 @@ - New API endpoints over projects to create, rename, edit, delete and mix tags. - Tag color assignation is not automatic. - Select a color (or not) to a tag when add it to stories, issues and tasks. -- Now comment owners and project admins can edit existing comments with the history Entry endpoint. -- Add a new permissions to allow add comments instead of use the existent modify permission for this purpose. -- Include created, modified and finished dates for tasks in CSV reports -- User stories and tasks listing API call support extra params to include more data (tasks and attachemnts and attachments, respectively) +- Improve search system over stories, tasks and issues: + - Search into tags too. (thanks to [Riccardo Cocciol](https://github.com/volans-)) + - Weights are applied: (subject = ref > tags > description). - Import/Export: - Gzip export/import support. - Export performance improvements. @@ -32,7 +33,7 @@ ## 2.1.0 Ursus Americanus (2016-05-03) ### Features -- Add sprint name and slug on search results for user stories ((thanks to [@everblut](https://github.com/everblut))) +- Add sprint name and slug on search results for user stories (thanks to [@everblut](https://github.com/everblut)) - [API] projects resource: Random order if `discover_mode=true` and `is_featured=true`. - Webhooks: Improve webhook data: - add permalinks diff --git a/taiga/searches/services.py b/taiga/searches/services.py index 5e04518a..afc6a7e5 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -25,58 +25,65 @@ MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) def search_user_stories(project, text): - model_cls = apps.get_model("userstories", "UserStory") - where_clause = ("to_tsvector('english_nostop', coalesce(userstories_userstory.subject) || ' ' || " - "coalesce(userstories_userstory.ref) || ' ' || " - "coalesce(array_to_string(userstories_userstory.tags, ' '), '') || ' ' || " - "coalesce(userstories_userstory.description, '')) " - "@@ to_tsquery('english_nostop', %s)") - - queryset = model_cls.objects.filter(project_id=project.pk) - - if text: - queryset = queryset.extra(where=[where_clause], params=[to_tsquery(text)]) - - queryset = attach_total_points(queryset) - return queryset[:MAX_RESULTS] + model = apps.get_model("userstories", "UserStory") + queryset = model.objects.filter(project_id=project.pk) + table = "userstories_userstory" + return _search_items(queryset, table, text) def search_tasks(project, text): - model_cls = apps.get_model("tasks", "Task") - where_clause = ("to_tsvector('english_nostop', coalesce(tasks_task.subject, '') || ' ' || " - "coalesce(tasks_task.ref) || ' ' || " - "coalesce(array_to_string(tasks_task.tags, ' '), '') || ' ' || " - "coalesce(tasks_task.description, '')) @@ to_tsquery('english_nostop', %s)") - - if text: - return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) - .filter(project_id=project.pk)[:MAX_RESULTS]) - - return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] + model = apps.get_model("userstories", "UserStory") + queryset = model.objects.filter(project_id=project.pk) + table = "userstories_userstory" + return _search_items(queryset, table, text) def search_issues(project, text): - model_cls = apps.get_model("issues", "Issue") - where_clause = ("to_tsvector('english_nostop', coalesce(issues_issue.subject) || ' ' || " - "coalesce(issues_issue.ref) || ' ' || " - "coalesce(array_to_string(issues_issue.tags, ' '), '') || ' ' || " - "coalesce(issues_issue.description, '')) @@ to_tsquery('english_nostop', %s)") - - if text: - return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) - .filter(project_id=project.pk)[:MAX_RESULTS]) - - return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] + model = apps.get_model("userstories", "UserStory") + queryset = model.objects.filter(project_id=project.pk) + table = "userstories_userstory" + return _search_items(queryset, table, text) def search_wiki_pages(project, text): - model_cls = apps.get_model("wiki", "WikiPage") - where_clause = ("to_tsvector('english_nostop', coalesce(wiki_wikipage.slug) || ' ' || " - "coalesce(wiki_wikipage.content, '')) " - "@@ to_tsquery('english_nostop', %s)") + model = apps.get_model("wiki", "WikiPage") + queryset = model.objects.filter(project_id=project.pk) + tsquery = "to_tsquery('english_nostop', %s)" + tsvector = """ + setweight(to_tsvector('english_nostop', coalesce(wiki_wikipage.slug)), 'A') || + setweight(to_tsvector('english_nostop', coalesce(wiki_wikipage.content)), 'B') + """ + + return _search_by_query(queryset, tsquery, tsvector, text) + + +def _search_items(queryset, table, text): + tsquery = "to_tsquery('english_nostop', %s)" + tsvector = """ + setweight(to_tsvector('english_nostop', + coalesce({table}.subject) || ' ' || + coalesce({table}.ref)), 'A') || + setweight(to_tsvector('english_nostop', coalesce(inmutable_array_to_string({table}.tags))), 'B') || + setweight(to_tsvector('english_nostop', coalesce({table}.description)), 'C') + """.format(table=table) + return _search_by_query(queryset, tsquery, tsvector, text) + + +def _search_by_query(queryset, tsquery, tsvector, text): + select = { + "rank": "ts_rank({tsvector},{tsquery})".format(tsquery=tsquery, + tsvector=tsvector), + } + order_by = ["-rank", ] + where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery, + tsvector=tsvector), ] if text: - return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) - .filter(project_id=project.pk)[:MAX_RESULTS]) + queryset = queryset.extra(select=select, + select_params=[to_tsquery(text)], + where=where, + params=[to_tsquery(text)], + order_by=order_by) - return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] + queryset = attach_total_points(queryset) + return queryset[:MAX_RESULTS] From bbf0ea370e2387ef0de36995b209679e01cbee43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 8 Jul 2016 20:10:16 +0200 Subject: [PATCH 105/261] Prevent accidental deletions of the database --- regenerate.sh | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/regenerate.sh b/regenerate.sh index 47f9c962..0fff7efe 100755 --- a/regenerate.sh +++ b/regenerate.sh @@ -1,6 +1,25 @@ #!/bin/bash -# For postgresql +show_answer=true +while [ $# -gt 0 ]; do + case "$1" in + -y) + show_answer=false + ;; + esac + shift +done + +if $show_answer ; then + echo "WARNING!! This script REMOVE your Taiga's database and you LOSE all the data." + read -p "Are you sure you want to delete all data? (Pres Y to continue): " -n 1 -r + echo # (optional) move to a new line + if [[ ! $REPLY =~ ^[Yy]$ ]] ; then + exit 1 + fi +fi + + echo "-> Remove taiga DB" dropdb taiga echo "-> Create taiga DB" From f1a55e747d65a698e24baf25a23caf4259494e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 8 Jul 2016 20:25:01 +0200 Subject: [PATCH 106/261] Remove a print --- taiga/base/api/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index c8f55a60..a7efeadf 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -614,7 +614,6 @@ class ChoiceField(WritableField): def validate_user_email_allowed_domains(value): domain_name = value.split("@")[1] - print("EMAIL VALIDATE DOMAIN") if settings.USER_EMAIL_ALLOWED_DOMAINS and domain_name not in settings.USER_EMAIL_ALLOWED_DOMAINS: raise ValidationError(_("You email domain is not allowed")) From f62952ac003a4e7c5169891c2b0956c800cd6b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 8 Jul 2016 20:37:43 +0200 Subject: [PATCH 107/261] Fix a typo --- regenerate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/regenerate.sh b/regenerate.sh index 0fff7efe..ec153fa9 100755 --- a/regenerate.sh +++ b/regenerate.sh @@ -12,7 +12,7 @@ done if $show_answer ; then echo "WARNING!! This script REMOVE your Taiga's database and you LOSE all the data." - read -p "Are you sure you want to delete all data? (Pres Y to continue): " -n 1 -r + read -p "Are you sure you want to delete all data? (Press Y to continue): " -n 1 -r echo # (optional) move to a new line if [[ ! $REPLY =~ ^[Yy]$ ]] ; then exit 1 From 86348b472f69b714bd8a5cefb36ca7cd4c96d07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 18 Jul 2016 12:12:29 +0200 Subject: [PATCH 108/261] Fix tests --- tests/factories.py | 50 +++++++++---------- tests/integration/test_searches.py | 77 ++++++++++++++---------------- 2 files changed, 61 insertions(+), 66 deletions(-) diff --git a/tests/factories.py b/tests/factories.py index 132b007f..379556ca 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -229,20 +229,6 @@ class StorageEntryFactory(Factory): value = factory.Sequence(lambda n: {"value": "value-{}".format(n)}) -class UserStoryFactory(Factory): - class Meta: - model = "userstories.UserStory" - strategy = factory.CREATE_STRATEGY - - ref = factory.Sequence(lambda n: n) - project = factory.SubFactory("tests.factories.ProjectFactory") - owner = factory.SubFactory("tests.factories.UserFactory") - subject = factory.Sequence(lambda n: "User Story {}".format(n)) - description = factory.Sequence(lambda n: "User Story {} description".format(n)) - status = factory.SubFactory("tests.factories.UserStoryStatusFactory") - milestone = factory.SubFactory("tests.factories.MilestoneFactory") - - class UserStoryStatusFactory(Factory): class Meta: model = "projects.UserStoryStatus" @@ -273,21 +259,19 @@ class MilestoneFactory(Factory): estimated_finish = factory.LazyAttribute(lambda o: o.estimated_start + timedelta(days=7)) -class IssueFactory(Factory): +class UserStoryFactory(Factory): class Meta: - model = "issues.Issue" + model = "userstories.UserStory" strategy = factory.CREATE_STRATEGY ref = factory.Sequence(lambda n: n) - subject = factory.Sequence(lambda n: "Issue {}".format(n)) - description = factory.Sequence(lambda n: "Issue {} description".format(n)) - owner = factory.SubFactory("tests.factories.UserFactory") project = factory.SubFactory("tests.factories.ProjectFactory") - status = factory.SubFactory("tests.factories.IssueStatusFactory") - severity = factory.SubFactory("tests.factories.SeverityFactory") - priority = factory.SubFactory("tests.factories.PriorityFactory") - type = factory.SubFactory("tests.factories.IssueTypeFactory") + owner = factory.SubFactory("tests.factories.UserFactory") + subject = factory.Sequence(lambda n: "User Story {}".format(n)) + description = factory.Sequence(lambda n: "User Story {} description".format(n)) + status = factory.SubFactory("tests.factories.UserStoryStatusFactory") milestone = factory.SubFactory("tests.factories.MilestoneFactory") + tags = factory.Faker("words") class TaskFactory(Factory): @@ -303,7 +287,25 @@ class TaskFactory(Factory): status = factory.SubFactory("tests.factories.TaskStatusFactory") milestone = factory.SubFactory("tests.factories.MilestoneFactory") user_story = factory.SubFactory("tests.factories.UserStoryFactory") - tags = [] + tags = factory.Faker("words") + + +class IssueFactory(Factory): + class Meta: + model = "issues.Issue" + strategy = factory.CREATE_STRATEGY + + ref = factory.Sequence(lambda n: n) + subject = factory.Sequence(lambda n: "Issue {}".format(n)) + description = factory.Sequence(lambda n: "Issue {} description".format(n)) + owner = factory.SubFactory("tests.factories.UserFactory") + project = factory.SubFactory("tests.factories.ProjectFactory") + status = factory.SubFactory("tests.factories.IssueStatusFactory") + severity = factory.SubFactory("tests.factories.SeverityFactory") + priority = factory.SubFactory("tests.factories.PriorityFactory") + type = factory.SubFactory("tests.factories.IssueTypeFactory") + milestone = factory.SubFactory("tests.factories.MilestoneFactory") + tags = factory.Faker("words") class WikiPageFactory(Factory): diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index 342b14ea..1ccd5233 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -52,37 +52,30 @@ def searches_initial_data(): role__project=m.project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.RoleFactory(project=m.project2) - m.points1 = f.PointsFactory(project=m.project1, value=None) - m.points2 = f.PointsFactory(project=m.project2, value=None) + m.us11 = f.UserStoryFactory(project=m.project1, subject="Back to the future") + m.us12 = f.UserStoryFactory(project=m.project1, description="Back to the future") + m.us13 = f.UserStoryFactory(project=m.project1, tags=["Backend", "future"]) + m.us14 = f.UserStoryFactory(project=m.project1) + m.us21 = f.UserStoryFactory(project=m.project2, subject="Backend to the future") - m.role_points1 = f.RolePointsFactory.create(role=m.project1.roles.all()[0], - points=m.points1, - user_story__project=m.project1) - m.role_points2 = f.RolePointsFactory.create(role=m.project1.roles.all()[0], - points=m.points1, - user_story__project=m.project1, - user_story__description="Back to the future") - m.role_points3 = f.RolePointsFactory.create(role=m.project2.roles.all()[0], - points=m.points2, - user_story__project=m.project2) + m.task11 = f.TaskFactory(project=m.project1, subject="Back to the future") + m.task12 = f.TaskFactory(project=m.project1, tags=["Back", "future"]) + m.task13 = f.TaskFactory(project=m.project1) + m.task14 = f.TaskFactory(project=m.project1, description="Backend to the future") + m.task21 = f.TaskFactory(project=m.project2, subject="Back to the future") - m.us1 = m.role_points1.user_story - m.us2 = m.role_points2.user_story - m.us3 = m.role_points3.user_story + m.issue11 = f.IssueFactory(project=m.project1, description="Back to the future") + m.issue12 = f.IssueFactory(project=m.project1, tags=["back", "future"]) + m.issue13 = f.IssueFactory(project=m.project1) + m.issue14 = f.IssueFactory(project=m.project1, subject="Backend to the future") + m.issue21 = f.IssueFactory(project=m.project2, subject="Back to the future") - m.tsk1 = f.TaskFactory.create(project=m.project2) - m.tsk2 = f.TaskFactory.create(project=m.project1) - m.tsk3 = f.TaskFactory.create(project=m.project1, subject="Back to the future") - - m.iss1 = f.IssueFactory.create(project=m.project1, subject="Backend and Frontend") - m.iss2 = f.IssueFactory.create(project=m.project2) - m.iss3 = f.IssueFactory.create(project=m.project1) - - m.wiki1 = f.WikiPageFactory.create(project=m.project1) - m.wiki2 = f.WikiPageFactory.create(project=m.project1, content="Frontend, future") - m.wiki3 = f.WikiPageFactory.create(project=m.project2) + m.wikipage11 = f.WikiPageFactory(project=m.project1) + m.wikipage12 = f.WikiPageFactory(project=m.project1) + m.wikipage13 = f.WikiPageFactory(project=m.project1, content="Backend to the black") + m.wikipage14 = f.WikiPageFactory(project=m.project1, slug="Back to the black") + m.wikipage21 = f.WikiPageFactory(project=m.project2, slug="Backend to the orange") return m @@ -94,11 +87,11 @@ def test_search_all_objects_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id}) assert response.status_code == 200 - assert response.data["count"] == 8 - assert len(response.data["userstories"]) == 2 - assert len(response.data["tasks"]) == 2 - assert len(response.data["issues"]) == 2 - assert len(response.data["wikipages"]) == 2 + assert response.data["count"] == 16 + assert len(response.data["userstories"]) == 4 + assert len(response.data["tasks"]) == 4 + assert len(response.data["issues"]) == 4 + assert len(response.data["wikipages"]) == 4 def test_search_all_objects_in_project_is_not_mine(client, searches_initial_data): @@ -118,20 +111,20 @@ def test_search_text_query_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "future"}) assert response.status_code == 200 - assert response.data["count"] == 3 - assert len(response.data["userstories"]) == 1 - assert len(response.data["tasks"]) == 1 - assert len(response.data["issues"]) == 0 - assert len(response.data["wikipages"]) == 1 + assert response.data["count"] == 9 + assert len(response.data["userstories"]) == 3 + assert len(response.data["tasks"]) == 3 + assert len(response.data["issues"]) == 3 + assert len(response.data["wikipages"]) == 0 response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "back"}) assert response.status_code == 200 - assert response.data["count"] == 3 - assert len(response.data["userstories"]) == 1 - assert len(response.data["tasks"]) == 1 + assert response.data["count"] == 11 + assert len(response.data["userstories"]) == 3 + assert len(response.data["tasks"]) == 3 + assert len(response.data["issues"]) == 3 # Back is a backend substring - assert len(response.data["issues"]) == 1 - assert len(response.data["wikipages"]) == 0 + assert len(response.data["wikipages"]) == 2 def test_search_text_query_with_an_invalid_project_id(client, searches_initial_data): From 698b15722d8c330ae0503b2a6975e2b62b23d06f Mon Sep 17 00:00:00 2001 From: Andrey Arapov Date: Sun, 17 Jul 2016 01:31:38 +0200 Subject: [PATCH 109/261] Update bleach to 1.4.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1262fdc..47ad6c46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ celery==3.1.20 redis==2.10.5 Unidecode==0.04.19 raven==5.10.2 -bleach==1.4.2 +bleach==1.4.3 django-ipware==1.1.3 premailer==2.9.7 cssutils==1.0.1 # Compatible with python 3.5 From 2c5716a0970edce2f8e13ca55d2df30334692cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 18 Jul 2016 13:00:29 +0200 Subject: [PATCH 110/261] Fix more test --- tests/integration/test_projects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index cc3b3cb1..fbcc5e1e 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1930,7 +1930,7 @@ def test_edit_tag_only_name(client, settings): client.login(user) response = client.json.post(url, json.dumps(data)) - print(response.data) + assert response.status_code == 200 project = Project.objects.get(id=project.pk) assert project.tags_colors == [["renamed_tag", "#123123"]] @@ -2069,7 +2069,7 @@ def test_color_tags_project_fired_on_element_update(): user_story.tags = ["tag"] user_story.save() project = Project.objects.get(id=user_story.project.id) - assert project.tags_colors == [["tag", None]] + assert ["tag", None] in project.tags_colors def test_color_tags_project_fired_on_element_update_respecting_color(): @@ -2078,4 +2078,4 @@ def test_color_tags_project_fired_on_element_update_respecting_color(): user_story.tags = ["tag"] user_story.save() project = Project.objects.get(id=user_story.project.id) - assert project.tags_colors == [["tag", "#123123"]] + assert ["tag", "#123123"] in project.tags_colors From 25c74cfb0734be5f1d39af903e56b0ab242f15c6 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 20 Jul 2016 08:38:50 +0200 Subject: [PATCH 111/261] Improving HighLightedContentSerializer and making more consistent how the assigned_to extra info is shown --- taiga/users/serializers.py | 30 +++-------- taiga/users/services.py | 95 +++++++++++++++++---------------- tests/integration/test_users.py | 27 +++++----- 3 files changed, 71 insertions(+), 81 deletions(-) diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 76fa6141..5b81ac6f 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -26,7 +26,7 @@ from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project from .services import get_user_photo_url, get_big_photo_url, get_user_big_photo_url from taiga.users.gravatar import get_user_gravatar_id - +from taiga.users.models import User from collections import namedtuple @@ -182,10 +182,8 @@ class HighLightedContentSerializer(serializers.LightSerializer): project_is_private = MethodField() project_blocked_code = Field() - assigned_to_username = Field() - assigned_to_full_name = Field() - assigned_to_photo = MethodField() - assigned_to_gravatar_id = MethodField() + assigned_to = Field(attr="assigned_to_id") + assigned_to_extra_info = MethodField() is_watcher = MethodField() total_watchers = Field() @@ -235,23 +233,11 @@ class HighLightedContentSerializer(serializers.LightSerializer): return get_thumbnail_url(logo, settings.THN_LOGO_SMALL) return None - def get_assigned_to_photo(self, obj): - type = getattr(obj, "type", "") - if type == "project": - return None - - UserData = namedtuple("UserData", ["photo", "email"]) - user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "") - return get_user_photo_url(user_data) - - def get_assigned_to_gravatar_id(self, obj): - type = getattr(obj, "type", "") - if type == "project": - return None - - UserData = namedtuple("UserData", ["photo", "email"]) - user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "") - return get_user_gravatar_id(user_data) + def get_assigned_to_extra_info(self, obj): + assigned_to = None + if obj.assigned_to_extra_info is not None: + assigned_to = User(**obj.assigned_to_extra_info) + return UserBasicInfoSerializer(assigned_to).data def get_tags_colors(self, obj): tags = getattr(obj, "tags", []) diff --git a/taiga/users/services.py b/taiga/users/services.py index 98b813b1..5923ff11 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -54,7 +54,7 @@ def get_user_by_username_or_email(username_or_email): return user -def get_and_validate_user(*, username:str, password:str) -> bool: +def get_and_validate_user(*, username: str, password: str) -> bool: """ Check if user with username/email exists and specified password matchs well with existing user password. @@ -115,17 +115,17 @@ def get_visible_project_ids(from_user, by_user): # Authenticated if by_user.is_authenticated(): - #Calculating the projects wich from_user user is member + # Calculating the projects wich from_user user is member by_user_project_ids = by_user.memberships.values_list("project__id", flat=True) - #Adding to the condition two OR situations: - #- The from user has a role that allows access to the project - #- The to user is the owner + # Adding to the condition two OR situations: + # - The from user has a role that allows access to the project + # - The to user is the owner member_perm_conditions |= \ Q(project__id__in=by_user_project_ids, role__permissions__contains=required_permissions) |\ Q(project__id__in=by_user_project_ids, is_admin=True) Membership = apps.get_model('projects', 'Membership') - #Calculating the user memberships adding the permission filter for the by user + # Calculating the user memberships adding the permission filter for the by user memberships_qs = Membership.objects.filter(member_perm_conditions, user=from_user) project_ids = memberships_qs.values_list("project__id", flat=True) return project_ids @@ -137,8 +137,8 @@ def get_stats_for_user(from_user, by_user): total_num_projects = len(project_ids) - roles = [_(r) for r in from_user.memberships.filter(project__id__in=project_ids).values_list( - "role__name", flat=True)] + role_names = from_user.memberships.filter(project__id__in=project_ids).values_list("role__name", flat=True) + roles = [_(r) for r in role_names] roles = list(set(roles)) User = apps.get_model('users', 'User') @@ -210,9 +210,9 @@ def get_watched_content_for_user(user): list.append(object_id) user_watches[ct_model] = list - #Now for projects, + # Now for projects, projects_watched = get_projects_watched(user) - project_content_type_model=ContentType.objects.get(app_label="projects", model="project").model + project_content_type_model = ContentType.objects.get(app_label="projects", model="project").model user_watches[project_content_type_model] = projects_watched.values_list("id", flat=True) return user_watches @@ -220,22 +220,22 @@ def get_watched_content_for_user(user): def _build_watched_sql_for_projects(for_user): sql = """ - SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type, + SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type, tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project, slug, projects_project.name, null::text AS subject, notifications_notifypolicy.created_at as created_date, coalesce(watchers, 0) AS total_watchers, projects_project.total_fans AS total_fans, null::integer AS total_voters, null::integer AS assigned_to, null::text as status, null::text as status_color - FROM notifications_notifypolicy - INNER JOIN projects_project - ON (projects_project.id = notifications_notifypolicy.project_id) - LEFT JOIN (SELECT project_id, count(*) watchers + FROM notifications_notifypolicy + INNER JOIN projects_project + ON (projects_project.id = notifications_notifypolicy.project_id) + LEFT JOIN (SELECT project_id, count(*) watchers FROM notifications_notifypolicy WHERE notifications_notifypolicy.notify_level != {none_notify_level} GROUP BY project_id ) type_watchers - ON projects_project.id = type_watchers.project_id - WHERE + ON projects_project.id = type_watchers.project_id + WHERE notifications_notifypolicy.user_id = {for_user_id} AND notifications_notifypolicy.notify_level != {none_notify_level} """ @@ -248,22 +248,22 @@ def _build_watched_sql_for_projects(for_user): def _build_liked_sql_for_projects(for_user): sql = """ - SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type, + SELECT projects_project.id AS id, null::integer AS ref, 'project'::text AS type, tags, likes_like.object_id AS object_id, projects_project.id AS project, slug, projects_project.name, null::text AS subject, likes_like.created_date, coalesce(watchers, 0) AS total_watchers, projects_project.total_fans AS total_fans, null::integer AS assigned_to, null::text as status, null::text as status_color - FROM likes_like - INNER JOIN projects_project - ON (projects_project.id = likes_like.object_id) - LEFT JOIN (SELECT project_id, count(*) watchers + FROM likes_like + INNER JOIN projects_project + ON (projects_project.id = likes_like.object_id) + LEFT JOIN (SELECT project_id, count(*) watchers FROM notifications_notifypolicy WHERE notifications_notifypolicy.notify_level != {none_notify_level} GROUP BY project_id ) type_watchers - ON projects_project.id = type_watchers.project_id - WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id + ON projects_project.id = type_watchers.project_id + WHERE likes_like.user_id = {for_user_id} AND {project_content_type_id} = likes_like.content_type_id """ sql = sql.format( for_user_id=for_user.id, @@ -274,39 +274,38 @@ def _build_liked_sql_for_projects(for_user): def _build_sql_for_type(for_user, type, table_name, action_table, ref_column="ref", - project_column="project_id", assigned_to_column="assigned_to_id", - slug_column="slug", subject_column="subject"): + project_column="project_id", assigned_to_column="assigned_to_id", + slug_column="slug", subject_column="subject"): sql = """ - SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, + SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, tags, {action_table}.object_id AS object_id, {table_name}.{project_column} AS project, {slug_column} AS slug, null AS name, {subject_column} AS subject, {action_table}.created_date, coalesce(watchers, 0) AS total_watchers, null::integer AS total_fans, coalesce(votes_votes.count, 0) AS total_voters, {assigned_to_column} AS assigned_to, projects_{type}status.name as status, projects_{type}status.color as status_color - FROM {action_table} - INNER JOIN django_content_type - ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}') - INNER JOIN {table_name} - ON ({table_name}.id = {action_table}.object_id) + FROM {action_table} + INNER JOIN django_content_type + ON ({action_table}.content_type_id = django_content_type.id AND django_content_type.model = '{type}') + INNER JOIN {table_name} + ON ({table_name}.id = {action_table}.object_id) INNER JOIN projects_{type}status - ON (projects_{type}status.id = {table_name}.status_id) - LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers - ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id - LEFT JOIN votes_votes - ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id) - WHERE {action_table}.user_id = {for_user_id} + ON (projects_{type}status.id = {table_name}.status_id) + LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers + ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id + LEFT JOIN votes_votes + ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id) + WHERE {action_table}.user_id = {for_user_id} """ sql = sql.format(for_user_id=for_user.id, type=type, table_name=table_name, - action_table=action_table, ref_column = ref_column, - project_column=project_column, assigned_to_column=assigned_to_column, - slug_column=slug_column, subject_column=subject_column) + action_table=action_table, ref_column=ref_column, + project_column=project_column, assigned_to_column=assigned_to_column, + slug_column=slug_column, subject_column=subject_column) return sql def get_watched_list(for_user, from_user, type=None, q=None): filters_sql = "" - and_needed = False if type: filters_sql += " AND type = %(type)s " @@ -322,7 +321,9 @@ def get_watched_list(for_user, from_user, type=None, q=None): SELECT entities.*, projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo, - users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email + users_user.id as assigned_to_id, + row_to_json(users_user) as assigned_to_extra_info + FROM ( {userstories_sql} UNION @@ -401,7 +402,6 @@ def get_watched_list(for_user, from_user, type=None, q=None): def get_liked_list(for_user, from_user, type=None, q=None): filters_sql = "" - and_needed = False if type: filters_sql += " AND type = %(type)s " @@ -417,7 +417,8 @@ def get_liked_list(for_user, from_user, type=None, q=None): SELECT entities.*, projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo, - users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email + users_user.id as assigned_to_id, + row_to_json(users_user) as assigned_to_extra_info FROM ( {projects_sql} ) as entities @@ -484,7 +485,6 @@ def get_liked_list(for_user, from_user, type=None, q=None): def get_voted_list(for_user, from_user, type=None, q=None): filters_sql = "" - and_needed = False if type: filters_sql += " AND type = %(type)s " @@ -500,7 +500,8 @@ def get_voted_list(for_user, from_user, type=None, q=None): SELECT entities.*, projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, projects_project.blocked_code as project_blocked_code, projects_project.tags_colors, projects_project.logo, - users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email + users_user.id as assigned_to_id, + row_to_json(users_user) as assigned_to_extra_info FROM ( {userstories_sql} UNION diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 79f78d11..c9bf5fba 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -558,9 +558,8 @@ def test_get_watched_list_valid_info_for_project(): assert project_watch_info["project_slug"] == None assert project_watch_info["project_is_private"] == None assert project_watch_info["project_blocked_code"] == None - assert project_watch_info["assigned_to_username"] == None - assert project_watch_info["assigned_to_full_name"] == None - assert project_watch_info["assigned_to_photo"] == None + assert project_watch_info["assigned_to"] == None + assert project_watch_info["assigned_to_extra_info"] == None def test_get_watched_list_for_project_with_ignored_notify_level(): @@ -613,9 +612,8 @@ def test_get_liked_list_valid_info(): assert project_like_info["project_slug"] == None assert project_like_info["project_is_private"] == None assert project_like_info["project_blocked_code"] == None - assert project_like_info["assigned_to_username"] == None - assert project_like_info["assigned_to_full_name"] == None - assert project_like_info["assigned_to_photo"] == None + assert project_like_info["assigned_to"] == None + assert project_like_info["assigned_to_extra_info"] == None def test_get_watched_list_valid_info_for_not_project_types(): @@ -667,9 +665,11 @@ def test_get_watched_list_valid_info_for_not_project_types(): assert instance_watch_info["project_slug"] == instance.project.slug assert instance_watch_info["project_is_private"] == instance.project.is_private assert instance_watch_info["project_blocked_code"] == instance.project.blocked_code - assert instance_watch_info["assigned_to_username"] == instance.assigned_to.username - assert instance_watch_info["assigned_to_full_name"] == instance.assigned_to.full_name - assert instance_watch_info["assigned_to_photo"] != "" + assert instance_watch_info["assigned_to"] != None + assert instance_watch_info["assigned_to_extra_info"]["username"] == instance.assigned_to.username + assert instance_watch_info["assigned_to_extra_info"]["full_name_display"] == instance.assigned_to.get_full_name() + assert instance_watch_info["assigned_to_extra_info"]["photo"] == None + assert instance_watch_info["assigned_to_extra_info"]["gravatar_id"] != None def test_get_voted_list_valid_info(): @@ -724,9 +724,12 @@ def test_get_voted_list_valid_info(): assert instance_vote_info["project_slug"] == instance.project.slug assert instance_vote_info["project_is_private"] == instance.project.is_private assert instance_vote_info["project_blocked_code"] == instance.project.blocked_code - assert instance_vote_info["assigned_to_username"] == instance.assigned_to.username - assert instance_vote_info["assigned_to_full_name"] == instance.assigned_to.full_name - assert instance_vote_info["assigned_to_photo"] != "" + assert instance_vote_info["assigned_to"] != None + assert instance_vote_info["assigned_to_extra_info"]["username"] == instance.assigned_to.username + assert instance_vote_info["assigned_to_extra_info"]["full_name_display"] == instance.assigned_to.get_full_name() + assert instance_vote_info["assigned_to_extra_info"]["photo"] == None + assert instance_vote_info["assigned_to_extra_info"]["gravatar_id"] != None + def test_get_watched_list_with_liked_and_voted_objects(client): From e1bdb1571085a96ba252c1a9b1951d6978b89136 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 21 Jul 2016 11:49:26 +0200 Subject: [PATCH 112/261] Removing unnecesary code --- taiga/projects/userstories/serializers.py | 10 ---------- taiga/timeline/timeline_implementations.py | 21 ++++++++++++--------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index ef15eec6..b23d5708 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -112,7 +112,6 @@ class UserStoryListSerializer( class UserStorySerializer(UserStoryListSerializer): comment = MethodField() - origin_issue = MethodField() blocked_note_html = MethodField() description = Field() description_html = MethodField() @@ -121,15 +120,6 @@ class UserStorySerializer(UserStoryListSerializer): # NOTE: This method and field is necessary to historical comments work return "" - def get_origin_issue(self, obj): - if obj.generated_from_issue: - return { - "id": obj.generated_from_issue.id, - "ref": obj.generated_from_issue.ref, - "subject": obj.generated_from_issue.subject, - } - return None - def get_blocked_note_html(self, obj): return mdrender(obj.project, obj.blocked_note) diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py index cff785ad..e6065971 100644 --- a/taiga/timeline/timeline_implementations.py +++ b/taiga/timeline/timeline_implementations.py @@ -19,11 +19,12 @@ from taiga.timeline.service import register_timeline_implementation from . import service + @register_timeline_implementation("projects.project", "create") @register_timeline_implementation("projects.project", "change") @register_timeline_implementation("projects.project", "delete") def project_timeline(instance, extra_data={}): - result ={ + result = { "project": service.extract_project_info(instance), } result.update(extra_data) @@ -33,8 +34,8 @@ def project_timeline(instance, extra_data={}): @register_timeline_implementation("milestones.milestone", "create") @register_timeline_implementation("milestones.milestone", "change") @register_timeline_implementation("milestones.milestone", "delete") -def project_timeline(instance, extra_data={}): - result ={ +def milestone_timeline(instance, extra_data={}): + result = { "milestone": service.extract_milestone_info(instance), "project": service.extract_project_info(instance.project), } @@ -46,7 +47,7 @@ def project_timeline(instance, extra_data={}): @register_timeline_implementation("userstories.userstory", "change") @register_timeline_implementation("userstories.userstory", "delete") def userstory_timeline(instance, extra_data={}): - result ={ + result = { "userstory": service.extract_userstory_info(instance), "project": service.extract_project_info(instance.project), } @@ -62,7 +63,7 @@ def userstory_timeline(instance, extra_data={}): @register_timeline_implementation("issues.issue", "change") @register_timeline_implementation("issues.issue", "delete") def issue_timeline(instance, extra_data={}): - result ={ + result = { "issue": service.extract_issue_info(instance), "project": service.extract_project_info(instance.project), } @@ -74,7 +75,7 @@ def issue_timeline(instance, extra_data={}): @register_timeline_implementation("tasks.task", "change") @register_timeline_implementation("tasks.task", "delete") def task_timeline(instance, extra_data={}): - result ={ + result = { "task": service.extract_task_info(instance), "project": service.extract_project_info(instance.project), } @@ -85,11 +86,12 @@ def task_timeline(instance, extra_data={}): result.update(extra_data) return result + @register_timeline_implementation("wiki.wikipage", "create") @register_timeline_implementation("wiki.wikipage", "change") @register_timeline_implementation("wiki.wikipage", "delete") def wiki_page_timeline(instance, extra_data={}): - result ={ + result = { "wikipage": service.extract_wiki_page_info(instance), "project": service.extract_project_info(instance.project), } @@ -100,7 +102,7 @@ def wiki_page_timeline(instance, extra_data={}): @register_timeline_implementation("projects.membership", "create") @register_timeline_implementation("projects.membership", "delete") def membership_timeline(instance, extra_data={}): - result = { + result = { "user": service.extract_user_info(instance.user), "project": service.extract_project_info(instance.project), "role": service.extract_role_info(instance.role), @@ -108,9 +110,10 @@ def membership_timeline(instance, extra_data={}): result.update(extra_data) return result + @register_timeline_implementation("users.user", "create") def user_timeline(instance, extra_data={}): - result = { + result = { "user": service.extract_user_info(instance), } result.update(extra_data) From c68211250e4b09da80fb4ac4dce0fdfe2756d9e7 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 22 Jul 2016 09:24:45 +0200 Subject: [PATCH 113/261] Fixing webhook milestone serializer --- taiga/webhooks/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index 61eea6cb..cb85720e 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -307,6 +307,12 @@ class MilestoneSerializer(serializers.LightSerializer): def get_permalink(self, obj): return resolve_front_url("taskboard", obj.project.slug, obj.slug) + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) + ######################################################################## # User Story From d31c7b21d7ad1fe4f814f9e79760c0c37f991036 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 22 Jul 2016 09:32:00 +0200 Subject: [PATCH 114/261] Fixing webhooks description_diff --- taiga/webhooks/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index cb85720e..3525c973 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -121,9 +121,8 @@ class HistoryDiffField(Field): # taiga.projects.history.models.HistoryEntry.values_diff() ret = {} - for key, val in value.items(): - if key in ["attachments", "custom_attributes"]: + if key in ["attachments", "custom_attributes", "description_diff"]: ret[key] = val elif key == "points": ret[key] = {k: {"from": v[0], "to": v[1]} for k, v in val.items()} From bad32e5cba1702cc567252093ab155e2b5bdfaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 22 Jul 2016 10:53:52 +0200 Subject: [PATCH 115/261] Pepocho --- taiga/base/filters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 4aa8cb09..e70b8390 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -30,7 +30,6 @@ from taiga.base.utils.db import to_tsquery logger = logging.getLogger(__name__) - ##################################################################### # Base and Mixins ##################################################################### @@ -229,7 +228,7 @@ class MembersFilterBackend(PermissionBasedFilterBackend): project_id = int(request.QUERY_PARAMS["project"]) except: logger.error("Filtering project diferent value than an integer: {}".format( - request.QUERY_PARAMS["project"])) + request.QUERY_PARAMS["project"])) raise exc.BadRequest(_("'project' must be an integer value.")) if project_id: @@ -256,14 +255,14 @@ class MembersFilterBackend(PermissionBasedFilterBackend): q = Q(memberships__project_id__in=projects_list) | Q(id=request.user.id) - #If there is no selected project we want access to users from public projects + # If there is no selected project we want access to users from public projects if not project: q = q | Q(memberships__project__public_permissions__contains=[self.permission]) qs = qs.filter(q) else: - if project and not "view_project" in project.anon_permissions: + if project and "view_project" not in project.anon_permissions: qs = qs.none() qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission]) @@ -433,13 +432,14 @@ class WatchersFilter(FilterBackend): def filter_queryset(self, request, queryset, view): query_watchers = self._get_watchers_queryparams(request.QUERY_PARAMS) - model = queryset.model if query_watchers: WatchedModel = apps.get_model("notifications", "Watched") watched_type = ContentType.objects.get_for_model(queryset.model) try: - watched_ids = WatchedModel.objects.filter(content_type=watched_type, user__id__in=query_watchers).values_list("object_id", flat=True) + watched_ids = (WatchedModel.objects.filter(content_type=watched_type, + user__id__in=query_watchers) + .values_list("object_id", flat=True)) queryset = queryset.filter(id__in=watched_ids) except ValueError: raise exc.BadRequest(_("Error in filter params types.")) From 1842e3dd9bd5dd746eddc4d7a3f2a3afcc7da8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 22 Jul 2016 12:21:09 +0200 Subject: [PATCH 116/261] Fix race condition on item creations and timeline inclusion --- taiga/timeline/signals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 9fc601da..1bee6c27 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -21,6 +21,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.utils import timezone from django.utils.translation import ugettext as _ +from django.db import connection from taiga.projects.history import services as history_services from taiga.projects.history.choices import HistoryType @@ -35,7 +36,7 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d ct = ContentType.objects.get_for_model(obj) if settings.CELERY_ENABLED: - push_to_timelines.delay(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data) + connection.on_commit(lambda: push_to_timelines.delay(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data)) else: push_to_timelines(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data) From ecc4b97fa10a4b8dc12480641e8c76984d978eb2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 27 Jul 2016 12:53:47 +0200 Subject: [PATCH 117/261] Adding id field to custom attribute serializer --- taiga/projects/custom_attributes/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index d4fc084e..afc5ff72 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -26,6 +26,7 @@ from taiga.base.api import serializers ####################################################### class BaseCustomAttributeSerializer(serializers.LightSerializer): + id = Field() name = Field() description = Field() type = Field() From 1fb7d530bd25aeb485ade4dde4c537feddb1ca7e Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 28 Jul 2016 10:00:15 +0200 Subject: [PATCH 118/261] Fixing custom fields deletes --- .../migrations/0008_auto_20160728_0540.py | 118 ++++++++++++++++++ taiga/projects/custom_attributes/models.py | 3 + 2 files changed, 121 insertions(+) create mode 100644 taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py diff --git a/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py b/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py new file mode 100644 index 00000000..6f2d86f7 --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-28 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_attributes', '0007_auto_20160208_1751'), + ] + + operations = [ + # Function: Remove a key in a json field + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) + RETURNS json + LANGUAGE sql + IMMUTABLE + STRICT + AS $function$ + SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}') + FROM json_each("json") + WHERE "key" <> ALL ("keys_to_delete")), + '{}')::json $function$; + """, + reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) + CASCADE;""" + ), + + # Function: Romeve a key in the json field of *_custom_attributes_values.values + migrations.RunSQL( + """ + CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"() + RETURNS trigger + AS $clean_key_in_custom_attributes_values$ + DECLARE + key text; + project_id int; + object_id int; + attribute text; + tablename text; + custom_attributes_tablename text; + BEGIN + key := OLD.id::text; + project_id := OLD.project_id; + attribute := TG_ARGV[0]::text; + tablename := TG_ARGV[1]::text; + custom_attributes_tablename := TG_ARGV[2]::text; + + EXECUTE 'UPDATE ' || quote_ident(custom_attributes_tablename) || ' + SET attributes_values = json_object_delete_keys(attributes_values, ' || quote_literal(key) || ') + FROM ' || quote_ident(tablename) || ' + WHERE ' || quote_ident(tablename) || '.project_id = ' || project_id || ' + AND ' || quote_ident(custom_attributes_tablename) || '.' || quote_ident(attribute) || ' = ' || quote_ident(tablename) || '.id'; + RETURN NULL; + END; $clean_key_in_custom_attributes_values$ + LANGUAGE plpgsql; + + """ + ), + + # Trigger: Clean userstorycustomattributes values before remove a userstorycustomattribute + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS "update_userstorycustomvalues_after_remove_userstorycustomattribute" + ON custom_attributes_userstorycustomattribute + CASCADE; + + CREATE TRIGGER "update_userstorycustomvalues_after_remove_userstorycustomattribute" + AFTER DELETE ON custom_attributes_userstorycustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('user_story_id', 'userstories_userstory', 'custom_attributes_userstorycustomattributesvalues'); + """ + ), + + # Trigger: Clean taskcustomattributes values before remove a taskcustomattribute + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS "update_taskcustomvalues_after_remove_taskcustomattribute" + ON custom_attributes_taskcustomattribute + CASCADE; + + CREATE TRIGGER "update_taskcustomvalues_after_remove_taskcustomattribute" + AFTER DELETE ON custom_attributes_taskcustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('task_id', 'tasks_task', 'custom_attributes_taskcustomattributesvalues'); + """ + ), + + # Trigger: Clean issuecustomattributes values before remove a issuecustomattribute + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS "update_issuecustomvalues_after_remove_issuecustomattribute" + ON custom_attributes_issuecustomattribute + CASCADE; + + CREATE TRIGGER "update_issuecustomvalues_after_remove_issuecustomattribute" + AFTER DELETE ON custom_attributes_issuecustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('issue_id', 'issues_issue', 'custom_attributes_issuecustomattributesvalues'); + """ + ), + migrations.AlterIndexTogether( + name='issuecustomattributesvalues', + index_together=set([('issue',)]), + ), + migrations.AlterIndexTogether( + name='taskcustomattributesvalues', + index_together=set([('task',)]), + ), + migrations.AlterIndexTogether( + name='userstorycustomattributesvalues', + index_together=set([('user_story',)]), + ), + ] diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index d7e4e32c..5fe3c6a0 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -101,6 +101,7 @@ class UserStoryCustomAttributesValues(AbstractCustomAttributesValues): class Meta(AbstractCustomAttributesValues.Meta): verbose_name = "user story ustom attributes values" verbose_name_plural = "user story custom attributes values" + index_together = [("user_story",)] @property def project(self): @@ -116,6 +117,7 @@ class TaskCustomAttributesValues(AbstractCustomAttributesValues): class Meta(AbstractCustomAttributesValues.Meta): verbose_name = "task ustom attributes values" verbose_name_plural = "task custom attributes values" + index_together = [("task",)] @property def project(self): @@ -131,6 +133,7 @@ class IssueCustomAttributesValues(AbstractCustomAttributesValues): class Meta(AbstractCustomAttributesValues.Meta): verbose_name = "issue ustom attributes values" verbose_name_plural = "issue custom attributes values" + index_together = [("issue",)] @property def project(self): From 5856ad6410d4801f0eafea101a5344878bcc4861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Jul 2016 12:27:24 +0200 Subject: [PATCH 119/261] Refactor bitbucket/github/gitlab hooks --- taiga/hooks/bitbucket/api.py | 8 - taiga/hooks/bitbucket/event_hooks.py | 232 +++------- taiga/hooks/event_hooks.py | 238 ++++++++++- taiga/hooks/github/event_hooks.py | 239 +++-------- taiga/hooks/github/services.py | 17 - taiga/hooks/gitlab/api.py | 8 - taiga/hooks/gitlab/event_hooks.py | 195 +++------ taiga/hooks/gitlab/services.py | 16 - tests/integration/test_hooks_bitbucket.py | 170 +++++++- tests/integration/test_hooks_github.py | 79 +++- tests/integration/test_hooks_gitlab.py | 499 ++++++++++++++++------ 11 files changed, 1022 insertions(+), 679 deletions(-) diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py index 24fc478c..07e2829b 100644 --- a/taiga/hooks/bitbucket/api.py +++ b/taiga/hooks/bitbucket/api.py @@ -72,13 +72,5 @@ class BitBucketViewSet(BaseWebhookApiViewSet): return project_secret == secret_key - def _get_project(self, request): - project_id = request.GET.get("project", None) - try: - project = Project.objects.get(id=project_id) - return project - except Project.DoesNotExist: - return None - def _get_event_name(self, request): return request.META.get('HTTP_X_EVENT_KEY', None) diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py index 8737aaa7..67ffc3fd 100644 --- a/taiga/hooks/bitbucket/event_hooks.py +++ b/taiga/hooks/bitbucket/event_hooks.py @@ -18,181 +18,67 @@ import re -from django.utils.translation import ugettext as _ - -from taiga.base import exceptions as exc -from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus -from taiga.projects.issues.models import Issue -from taiga.projects.tasks.models import Task -from taiga.projects.userstories.models import UserStory -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 +from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook -class PushEventHook(BaseEventHook): - def process_event(self): - if self.payload is None: - return +class BaseBitBucketEventHook(): + platform = "BitBucket" + platform_slug = "bitbucket" + def replace_bitbucket_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" + + template = "\g<1>[BitBucket#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) + return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) + + +class IssuesEventHook(BaseBitBucketEventHook, BaseNewIssueEventHook): + def get_data(self): + description = self.payload.get('issue', {}).get('content', {}).get('raw', '') + project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) + return { + "number": self.payload.get('issue', {}).get('id', None), + "subject": self.payload.get('issue', {}).get('title', None), + "url": self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None), + "user_id": self.payload.get('actor', {}).get('uuid', None), + "user_name": self.payload.get('actor', {}).get('username', None), + "user_url": self.payload.get('actor', {}).get('links', {}).get('html', {}).get('href'), + "description": self.replace_bitbucket_references(project_url, description), + } + + +class IssueCommentEventHook(BaseBitBucketEventHook, BaseIssueCommentEventHook): + def get_data(self): + comment_message = self.payload.get('comment', {}).get('content', {}).get('raw', '') + project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) + issue_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None) + comment_id = self.payload.get('comment', {}).get('id', None) + comment_url = "{}#comment-{}".format(issue_url, comment_id) + return { + "number": self.payload.get('issue', {}).get('id', None), + 'url': issue_url, + 'user_id': self.payload.get('actor', {}).get('uuid', None), + 'user_name': self.payload.get('actor', {}).get('username', None), + 'user_url': self.payload.get('actor', {}).get('links', {}).get('html', {}).get('href'), + 'comment_url': comment_url, + 'comment_message': self.replace_bitbucket_references(project_url, comment_message) + } + + +class PushEventHook(BaseBitBucketEventHook, BasePushEventHook): + def get_data(self): + result = [] changes = self.payload.get("push", {}).get('changes', []) for change in filter(None, changes): - commits = change.get("commits", []) - if not commits: - continue - - for commit in commits: - message = commit.get("message", None) - if not message: - continue - - self._process_message(message, None) - - def _process_message(self, message, bitbucket_user): - """ - The message we will be looking for seems like - TG-XX #yyyyyy - Where: - XX: is the ref for us, issue or task - yyyyyy: is the status slug we are setting - """ - if message is None: - return - - p = re.compile("tg-(\d+) +#([-\w]+)") - for m in p.finditer(message.lower()): - ref = m.group(1) - status_slug = m.group(2) - self._change_status(ref, status_slug, bitbucket_user) - - def _change_status(self, ref, status_slug, bitbucket_user): - if Issue.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Issue - statusClass = IssueStatus - elif Task.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Task - statusClass = TaskStatus - elif UserStory.objects.filter(project=self.project, ref=ref).exists(): - modelClass = UserStory - statusClass = UserStoryStatus - else: - raise ActionSyntaxException(_("The referenced element doesn't exist")) - - element = modelClass.objects.get(project=self.project, ref=ref) - - try: - status = statusClass.objects.get(project=self.project, slug=status_slug) - except statusClass.DoesNotExist: - raise ActionSyntaxException(_("The status doesn't exist")) - - element.status = status - element.save() - - snapshot = take_snapshot(element, - comment=_("Status changed from BitBucket commit"), - user=get_bitbucket_user(bitbucket_user)) - send_notifications(element, history=snapshot) - - -def replace_bitbucket_references(project_url, wiki_text): - template = "\g<1>[BitBucket#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) - return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) - - -class IssuesEventHook(BaseEventHook): - def process_event(self): - number = self.payload.get('issue', {}).get('id', None) - subject = self.payload.get('issue', {}).get('title', None) - - bitbucket_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None) - - bitbucket_user_id = self.payload.get('actor', {}).get('user', {}).get('uuid', None) - bitbucket_user_name = self.payload.get('actor', {}).get('user', {}).get('username', None) - bitbucket_user_url = self.payload.get('actor', {}).get('user', {}).get('links', {}).get('html', {}).get('href') - - project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) - - description = self.payload.get('issue', {}).get('content', {}).get('raw', '') - description = replace_bitbucket_references(project_url, description) - - user = get_bitbucket_user(bitbucket_user_id) - - if not all([subject, bitbucket_url, project_url]): - raise ActionSyntaxException(_("Invalid issue information")) - - issue = Issue.objects.create( - project=self.project, - subject=subject, - description=description, - status=self.project.default_issue_status, - type=self.project.default_issue_type, - severity=self.project.default_severity, - priority=self.project.default_priority, - external_reference=['bitbucket', bitbucket_url], - owner=user - ) - take_snapshot(issue, user=user) - - if number and subject and bitbucket_user_name and bitbucket_user_url: - comment = _("Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} " - "\"See @{bitbucket_user_name}'s BitBucket profile\") " - "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} " - "\"Go to 'bb#{number} - {subject}'\"):\n\n" - "{description}").format(bitbucket_user_name=bitbucket_user_name, - bitbucket_user_url=bitbucket_user_url, - number=number, - subject=subject, - bitbucket_url=bitbucket_url, - description=description) - else: - comment = _("Issue created from BitBucket.") - - snapshot = take_snapshot(issue, comment=comment, user=user) - send_notifications(issue, history=snapshot) - - -class IssueCommentEventHook(BaseEventHook): - def process_event(self): - number = self.payload.get('issue', {}).get('id', None) - subject = self.payload.get('issue', {}).get('title', None) - - bitbucket_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None) - bitbucket_user_id = self.payload.get('actor', {}).get('user', {}).get('uuid', None) - bitbucket_user_name = self.payload.get('actor', {}).get('user', {}).get('username', None) - bitbucket_user_url = self.payload.get('actor', {}).get('user', {}).get('links', {}).get('html', {}).get('href') - - project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) - - comment_message = self.payload.get('comment', {}).get('content', {}).get('raw', '') - comment_message = replace_bitbucket_references(project_url, comment_message) - - user = get_bitbucket_user(bitbucket_user_id) - - if not all([comment_message, bitbucket_url, project_url]): - raise ActionSyntaxException(_("Invalid issue comment information")) - - issues = Issue.objects.filter(external_reference=["bitbucket", bitbucket_url]) - tasks = Task.objects.filter(external_reference=["bitbucket", bitbucket_url]) - uss = UserStory.objects.filter(external_reference=["bitbucket", bitbucket_url]) - - for item in list(issues) + list(tasks) + list(uss): - if number and subject and bitbucket_user_name and bitbucket_user_url: - comment = _("Comment by [@{bitbucket_user_name}]({bitbucket_user_url} " - "\"See @{bitbucket_user_name}'s BitBucket profile\") " - "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} " - "\"Go to 'bb#{number} - {subject}'\")\n\n" - "{message}").format(bitbucket_user_name=bitbucket_user_name, - bitbucket_user_url=bitbucket_user_url, - number=number, - subject=subject, - bitbucket_url=bitbucket_url, - message=comment_message) - else: - comment = _("Comment From BitBucket:\n\n{message}").format(message=comment_message) - - snapshot = take_snapshot(item, comment=comment, user=user) - send_notifications(item, history=snapshot) + for commit in change.get("commits", []): + message = commit.get("message") + result.append({ + 'user_id': commit.get('author', {}).get('user', {}).get('uuid', None), + "user_name": commit.get('author', {}).get('user', {}).get('username', None), + "user_url": commit.get('author', {}).get('user', {}).get('links', {}).get('html', {}).get('href'), + "commit_id": commit.get("hash", None), + "commit_url": commit.get("links", {}).get('html', {}).get('href'), + "commit_message": message.strip(), + }) + return result diff --git a/taiga/hooks/event_hooks.py b/taiga/hooks/event_hooks.py index f4f6d2e8..85ac892f 100644 --- a/taiga/hooks/event_hooks.py +++ b/taiga/hooks/event_hooks.py @@ -16,11 +16,247 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import re + +from django.utils.translation import ugettext as _ +from django.contrib.auth import get_user_model +from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus +from taiga.projects.issues.models import Issue +from taiga.projects.tasks.models import Task +from taiga.projects.userstories.models import UserStory +from taiga.projects.history.services import take_snapshot +from taiga.projects.notifications.services import send_notifications +from taiga.hooks.exceptions import ActionSyntaxException +from taiga.users.models import AuthData + class BaseEventHook: + platform = "Unknown" + platform_slug = "unknown" + def __init__(self, project, payload): self.project = project self.payload = payload + def ignore(self): + return False + + def get_user(self, user_id, platform): + user = None + + if user_id: + try: + user = AuthData.objects.get(key=platform, value=user_id).user + except AuthData.DoesNotExist: + pass + + if user is None: + user = get_user_model().objects.get(is_system=True, username__startswith=platform) + + return user + + +class BaseIssueCommentEventHook(BaseEventHook): + def get_data(self): + raise NotImplementedError + + def generate_issue_comment_message(self, **kwargs): + _issue_comment_message = _( + "[@{user_name}]({user_url} " + "\"See @{user_name}'s {platform} profile\") " + "says in [{platform}#{number}]({comment_url} \"Go to comment\"):\n\n" + "\"{comment_message}\"" + ) + _simple_issue_comment_message = _("Comment From {platform}:\n\n> {comment_message}") + try: + return _issue_comment_message.format(platform=self.platform, **kwargs) + except Exception: + return _simple_issue_comment_message.format(platform=self.platform, message=kwargs.get('comment_message')) + def process_event(self): - raise NotImplementedError("process_event must be overwritten") + if self.ignore(): + return + + data = self.get_data() + + if not all([data['comment_message'], data['url']]): + raise ActionSyntaxException(_("Invalid issue comment information")) + + comment = self.generate_issue_comment_message(**data) + + issues = Issue.objects.filter(external_reference=[self.platform_slug, data['url']]) + tasks = Task.objects.filter(external_reference=[self.platform_slug, data['url']]) + uss = UserStory.objects.filter(external_reference=[self.platform_slug, data['url']]) + + for item in list(issues) + list(tasks) + list(uss): + snapshot = take_snapshot(item, comment=comment, user=self.get_user(data['user_id'], self.platform_slug)) + send_notifications(item, history=snapshot) + + +class BaseNewIssueEventHook(BaseEventHook): + def get_data(self): + raise NotImplementedError + + def generate_new_issue_comment(self, **kwargs): + _new_issue_message = _( + "Issue created by [@{user_name}]({user_url} " + "\"See @{user_name}'s {platform} profile\") " + "from [{platform}#{number}]({url} \"Go to issue\")." + ) + _simple_new_issue_message = _("Issue created from {platform}.") + try: + return _new_issue_message.format(platform=self.platform, **kwargs) + except Exception: + return _simple_new_issue_message.format(platform=self.platform) + + def process_event(self): + if self.ignore(): + return + + data = self.get_data() + + if not all([data['subject'], data['url']]): + raise ActionSyntaxException(_("Invalid issue information")) + + user = self.get_user(data['user_id'], self.platform_slug) + + issue = Issue.objects.create( + project=self.project, + subject=data['subject'], + description=data['description'], + status=self.project.default_issue_status, + type=self.project.default_issue_type, + severity=self.project.default_severity, + priority=self.project.default_priority, + external_reference=[self.platform_slug, data['url']], + owner=user + ) + take_snapshot(issue, user=user) + + comment = self.generate_new_issue_comment(**data) + + snapshot = take_snapshot(issue, comment=comment, user=user) + send_notifications(issue, history=snapshot) + + +class BasePushEventHook(BaseEventHook): + def get_data(self): + raise NotImplementedError + + def generate_status_change_comment(self, **kwargs): + if kwargs.get('user_url', None) is None: + user_text = kwargs.get('user_name', _('unknown user')) + else: + user_text = "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\")".format( + platform=self.platform, + **kwargs + ) + _status_change_message = _( + "{user_text} changed the status from " + "[{platform} commit]({commit_url} \"See commit '{commit_id} - {commit_message}'\")\n\n" + " - Status: **{src_status}** → **{dst_status}**" + ) + _simple_status_change_message = _( + "Changed status from {platform} commit.\n\n" + " - Status: **{src_status}** → **{dst_status}**" + ) + try: + return _status_change_message.format(platform=self.platform, user_text=user_text, **kwargs) + except Exception: + return _simple_status_change_message.format(platform=self.platform) + + def generate_commit_reference_comment(self, **kwargs): + if kwargs.get('user_url', None) is None: + user_text = kwargs.get('user_name', _('unknown user')) + else: + user_text = "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\")".format( + platform=self.platform, + **kwargs + ) + + _status_change_message = _( + "This {type_name} has been mentioned by {user_text} " + "in the [{platform} commit]({commit_url} \"See commit '{commit_id} - {commit_message}'\") " + "\"{commit_message}\"" + ) + _simple_status_change_message = _( + "This issue has been mentioned in the {platform} commit " + "\"{commit_message}\"" + ) + try: + return _status_change_message.format(platform=self.platform, user_text=user_text, **kwargs) + except Exception: + return _simple_status_change_message.format(platform=self.platform) + + def get_item_classes(self, ref): + if Issue.objects.filter(project=self.project, ref=ref).exists(): + modelClass = Issue + statusClass = IssueStatus + elif Task.objects.filter(project=self.project, ref=ref).exists(): + modelClass = Task + statusClass = TaskStatus + elif UserStory.objects.filter(project=self.project, ref=ref).exists(): + modelClass = UserStory + statusClass = UserStoryStatus + else: + raise ActionSyntaxException(_("The referenced element doesn't exist")) + + return (modelClass, statusClass) + + def get_item_by_ref(self, ref): + (modelClass, statusClass) = self.get_item_classes(ref) + + return modelClass.objects.get(project=self.project, ref=ref) + + def set_item_status(self, ref, status_slug): + (modelClass, statusClass) = self.get_item_classes(ref) + element = modelClass.objects.get(project=self.project, ref=ref) + + try: + status = statusClass.objects.get(project=self.project, slug=status_slug) + except statusClass.DoesNotExist: + raise ActionSyntaxException(_("The status doesn't exist")) + + src_status = element.status.name + dst_status = status.name + + element.status = status + element.save() + return (element, src_status, dst_status) + + def process_event(self): + if self.ignore(): + return + data = self.get_data() + + for commit in data: + consumed_refs = [] + + # Status changes + p = re.compile("tg-(\d+) +#([-\w]+)") + for m in p.finditer(commit['commit_message'].lower()): + ref = m.group(1) + status_slug = m.group(2) + (element, src_status, dst_status) = self.set_item_status(ref, status_slug) + + comment = self.generate_status_change_comment(src_status=src_status, dst_status=dst_status, **commit) + snapshot = take_snapshot(element, + comment=comment, + user=self.get_user(commit['user_id'], self.platform_slug)) + send_notifications(element, history=snapshot) + consumed_refs.append(ref) + + # Reference on commit + p = re.compile("tg-(\d+)") + for m in p.finditer(commit['commit_message'].lower()): + ref = m.group(1) + if ref in consumed_refs: + continue + element = self.get_item_by_ref(ref) + type_name = element.__class__._meta.verbose_name + comment = self.generate_commit_reference_comment(type_name=type_name, **commit) + snapshot = take_snapshot(element, + comment=comment, + user=self.get_user(commit['user_id'], self.platform_slug)) + send_notifications(element, history=snapshot) + consumed_refs.append(ref) diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py index 68e57993..c4ecc300 100644 --- a/taiga/hooks/github/event_hooks.py +++ b/taiga/hooks/github/event_hooks.py @@ -16,201 +16,72 @@ # 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 IssueStatus, TaskStatus, UserStoryStatus - -from taiga.projects.issues.models import Issue -from taiga.projects.tasks.models import Task -from taiga.projects.userstories.models import UserStory -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 .services import get_github_user - import re - -class PushEventHook(BaseEventHook): - def process_event(self): - if self.payload is None: - return - - github_user = self.payload.get('sender', {}) - - commits = self.payload.get("commits", []) - for commit in commits: - self._process_commit(commit, github_user) - - def _process_commit(self, commit, github_user): - """ - The message we will be looking for seems like - TG-XX #yyyyyy - Where: - XX: is the ref for us, issue or task - yyyyyy: is the status slug we are setting - """ - message = commit.get("message", None) - - if message is None: - return - - p = re.compile("tg-(\d+) +#([-\w]+)") - for m in p.finditer(message.lower()): - ref = m.group(1) - status_slug = m.group(2) - self._change_status(ref, status_slug, github_user, commit) - - def _change_status(self, ref, status_slug, github_user, commit): - if Issue.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Issue - statusClass = IssueStatus - elif Task.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Task - statusClass = TaskStatus - elif UserStory.objects.filter(project=self.project, ref=ref).exists(): - modelClass = UserStory - statusClass = UserStoryStatus - else: - raise ActionSyntaxException(_("The referenced element doesn't exist")) - - element = modelClass.objects.get(project=self.project, ref=ref) - - try: - status = statusClass.objects.get(project=self.project, slug=status_slug) - except statusClass.DoesNotExist: - raise ActionSyntaxException(_("The status doesn't exist")) - - element.status = status - element.save() - - github_user_id = github_user.get('id', None) - github_user_name = github_user.get('login', None) - github_user_url = github_user.get('html_url', None) - commit_id = commit.get("id", None) - commit_url = commit.get("url", None) - commit_message = commit.get("message", None) - - if (github_user_id and github_user_name and github_user_url and - commit_id and commit_url and commit_message): - comment = _("Status changed by [@{github_user_name}]({github_user_url} " - "\"See @{github_user_name}'s GitHub profile\") " - "from GitHub commit [{commit_id}]({commit_url} " - "\"See commit '{commit_id} - {commit_message}'\").").format( - github_user_name=github_user_name, - github_user_url=github_user_url, - commit_id=commit_id[:7], - commit_url=commit_url, - commit_message=commit_message) - - else: - comment = _("Status changed from GitHub commit.") - - snapshot = take_snapshot(element, - comment=comment, - user=get_github_user(github_user_id)) - send_notifications(element, history=snapshot) +from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook -def replace_github_references(project_url, wiki_text): - if wiki_text == None: - wiki_text = "" +class BaseGitHubEventHook(): + platform = "GitHub" + platform_slug = "github" - 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) + def replace_github_references(self, project_url, wiki_text): + if wiki_text is 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) -class IssuesEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('action', None) != "opened": - return +class IssuesEventHook(BaseGitHubEventHook, BaseNewIssueEventHook): + def ignore(self): + return self.payload.get('action', None) != "opened" - number = self.payload.get('issue', {}).get('number', None) - subject = self.payload.get('issue', {}).get('title', None) - github_url = self.payload.get('issue', {}).get('html_url', None) - github_user_id = self.payload.get('issue', {}).get('user', {}).get('id', None) - github_user_name = self.payload.get('issue', {}).get('user', {}).get('login', None) - github_user_url = self.payload.get('issue', {}).get('user', {}).get('html_url', None) - project_url = self.payload.get('repository', {}).get('html_url', None) + def get_data(self): description = self.payload.get('issue', {}).get('body', None) - description = replace_github_references(project_url, description) - - user = get_github_user(github_user_id) - - if not all([subject, github_url, project_url]): - raise ActionSyntaxException(_("Invalid issue information")) - - issue = Issue.objects.create( - project=self.project, - subject=subject, - description=description, - status=self.project.default_issue_status, - type=self.project.default_issue_type, - severity=self.project.default_severity, - priority=self.project.default_priority, - external_reference=['github', github_url], - owner=user - ) - take_snapshot(issue, user=user) - - if number and subject and github_user_name and github_user_url: - comment = _("Issue created by [@{github_user_name}]({github_user_url} " - "\"See @{github_user_name}'s GitHub profile\") " - "from GitHub.\nOrigin GitHub issue: [gh#{number} - {subject}]({github_url} " - "\"Go to 'gh#{number} - {subject}'\"):\n\n" - "{description}").format(github_user_name=github_user_name, - github_user_url=github_user_url, - number=number, - subject=subject, - github_url=github_url, - description=description) - else: - comment = _("Issue created from GitHub.") - - snapshot = take_snapshot(issue, comment=comment, user=user) - send_notifications(issue, history=snapshot) - - -class IssueCommentEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('action', None) != "created": - raise ActionSyntaxException(_("Invalid issue comment information")) - - number = self.payload.get('issue', {}).get('number', None) - subject = self.payload.get('issue', {}).get('title', None) - github_url = self.payload.get('issue', {}).get('html_url', None) - github_user_id = self.payload.get('sender', {}).get('id', None) - github_user_name = self.payload.get('sender', {}).get('login', None) - github_user_url = self.payload.get('sender', {}).get('html_url', None) project_url = self.payload.get('repository', {}).get('html_url', None) + return { + "number": self.payload.get('issue', {}).get('number', None), + "subject": self.payload.get('issue', {}).get('title', None), + "url": self.payload.get('issue', {}).get('html_url', None), + "user_id": self.payload.get('issue', {}).get('user', {}).get('id', None), + "user_name": self.payload.get('issue', {}).get('user', {}).get('login', None), + "user_url": self.payload.get('issue', {}).get('user', {}).get('html_url', None), + "description": self.replace_github_references(project_url, description), + } + + +class IssueCommentEventHook(BaseGitHubEventHook, BaseIssueCommentEventHook): + def ignore(self): + return self.payload.get('action', None) != "created" + + def get_data(self): comment_message = self.payload.get('comment', {}).get('body', None) - comment_message = replace_github_references(project_url, comment_message) + project_url = self.payload.get('repository', {}).get('html_url', None) + return { + "number": self.payload.get('issue', {}).get('number', None), + "url": self.payload.get('issue', {}).get('html_url', None), + "user_id": self.payload.get('sender', {}).get('id', None), + "user_name": self.payload.get('sender', {}).get('login', None), + "user_url": self.payload.get('sender', {}).get('html_url', None), + "comment_url": self.payload.get('comment', {}).get('html_url', None), + "comment_message": self.replace_github_references(project_url, comment_message), + } - user = get_github_user(github_user_id) - if not all([comment_message, github_url, project_url]): - raise ActionSyntaxException(_("Invalid issue comment information")) +class PushEventHook(BaseGitHubEventHook, BasePushEventHook): + def get_data(self): + result = [] + github_user = self.payload.get('sender', {}) + commits = self.payload.get("commits", []) + for commit in filter(None, commits): + result.append({ + "user_id": github_user.get('id', None), + "user_name": github_user.get('login', None), + "user_url": github_user.get('html_url', None), + "commit_id": commit.get("id", None), + "commit_url": commit.get("url", None), + "commit_message": commit.get("message", None), + }) - issues = Issue.objects.filter(external_reference=["github", github_url]) - tasks = Task.objects.filter(external_reference=["github", github_url]) - uss = UserStory.objects.filter(external_reference=["github", github_url]) - - for item in list(issues) + list(tasks) + list(uss): - if number and subject and github_user_name and github_user_url: - comment = _("Comment by [@{github_user_name}]({github_user_url} " - "\"See @{github_user_name}'s GitHub profile\") " - "from GitHub.\nOrigin GitHub issue: [gh#{number} - {subject}]({github_url} " - "\"Go to 'gh#{number} - {subject}'\")\n\n" - "{message}").format(github_user_name=github_user_name, - github_user_url=github_user_url, - number=number, - subject=subject, - github_url=github_url, - message=comment_message) - else: - comment = _("Comment From GitHub:\n\n{message}").format(message=comment_message) - - snapshot = take_snapshot(item, comment=comment, user=user) - send_notifications(item, history=snapshot) + return result diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py index cd244ae3..e7286d86 100644 --- a/taiga/hooks/github/services.py +++ b/taiga/hooks/github/services.py @@ -18,10 +18,8 @@ import uuid -from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse -from taiga.users.models import AuthData from taiga.base.utils.urls import get_absolute_url @@ -38,18 +36,3 @@ def get_or_generate_config(project): url = "%s?project=%s" % (url, project.id) g_config["webhooks_url"] = url return g_config - - -def get_github_user(github_id): - user = None - - if github_id: - try: - user = AuthData.objects.get(key="github", value=github_id).user - except AuthData.DoesNotExist: - pass - - if user is None: - user = get_user_model().objects.get(is_system=True, username__startswith="github") - - return user diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py index 910ee437..127d7536 100644 --- a/taiga/hooks/gitlab/api.py +++ b/taiga/hooks/gitlab/api.py @@ -70,14 +70,6 @@ class GitLabViewSet(BaseWebhookApiViewSet): return project_secret == secret_key - def _get_project(self, request): - project_id = request.GET.get("project", None) - try: - project = Project.objects.get(id=project_id) - return project - except Project.DoesNotExist: - return None - def _get_event_name(self, request): payload = json.loads(request.body.decode("utf-8")) return payload.get('object_kind', 'push') if payload is not None else 'empty' diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py index aff09e2f..5b4b4006 100644 --- a/taiga/hooks/gitlab/event_hooks.py +++ b/taiga/hooks/gitlab/event_hooks.py @@ -19,158 +19,71 @@ import re import os -from django.utils.translation import ugettext as _ - -from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus - -from taiga.projects.issues.models import Issue -from taiga.projects.tasks.models import Task -from taiga.projects.userstories.models import UserStory -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 .services import get_gitlab_user +from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook -class PushEventHook(BaseEventHook): - def process_event(self): - if self.payload is None: - return +class BaseGitLabEventHook(): + platform = "GitLab" + platform_slug = "gitlab" - commits = self.payload.get("commits", []) - for commit in commits: - message = commit.get("message", None) - self._process_message(message, None) + def replace_gitlab_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" - def _process_message(self, message, gitlab_user): - """ - The message we will be looking for seems like - TG-XX #yyyyyy - Where: - XX: is the ref for us, issue or task - yyyyyy: is the status slug we are setting - """ - if message is None: - return - - p = re.compile("tg-(\d+) +#([-\w]+)") - for m in p.finditer(message.lower()): - ref = m.group(1) - status_slug = m.group(2) - self._change_status(ref, status_slug, gitlab_user) - - def _change_status(self, ref, status_slug, gitlab_user): - if Issue.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Issue - statusClass = IssueStatus - elif Task.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Task - statusClass = TaskStatus - elif UserStory.objects.filter(project=self.project, ref=ref).exists(): - modelClass = UserStory - statusClass = UserStoryStatus - else: - raise ActionSyntaxException(_("The referenced element doesn't exist")) - - element = modelClass.objects.get(project=self.project, ref=ref) - - try: - status = statusClass.objects.get(project=self.project, slug=status_slug) - except statusClass.DoesNotExist: - raise ActionSyntaxException(_("The status doesn't exist")) - - element.status = status - element.save() - - snapshot = take_snapshot(element, - comment=_("Status changed from GitLab commit"), - user=get_gitlab_user(gitlab_user)) - send_notifications(element, history=snapshot) + 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) -def replace_gitlab_references(project_url, wiki_text): - if wiki_text is None: - wiki_text = "" +class IssuesEventHook(BaseGitLabEventHook, BaseNewIssueEventHook): + def ignore(self): + return self.payload.get('object_attributes', {}).get("action", "") != "open" - 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) - - -class IssuesEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('object_attributes', {}).get("action", "") != "open": - return - - subject = self.payload.get('object_attributes', {}).get('title', None) + def get_data(self): description = self.payload.get('object_attributes', {}).get('description', None) - gitlab_reference = self.payload.get('object_attributes', {}).get('url', None) - - project_url = None - if gitlab_reference: - project_url = os.path.basename(os.path.basename(gitlab_reference)) - - if not all([subject, gitlab_reference, project_url]): - raise ActionSyntaxException(_("Invalid issue information")) - - issue = Issue.objects.create( - project=self.project, - subject=subject, - description=replace_gitlab_references(project_url, description), - status=self.project.default_issue_status, - type=self.project.default_issue_type, - severity=self.project.default_severity, - priority=self.project.default_priority, - external_reference=['gitlab', gitlab_reference], - owner=get_gitlab_user(None) - ) - take_snapshot(issue, user=get_gitlab_user(None)) - - snapshot = take_snapshot(issue, comment=_("Created from GitLab"), user=get_gitlab_user(None)) - send_notifications(issue, history=snapshot) - - -class IssueCommentEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue": - return - - number = self.payload.get('issue', {}).get('iid', None) - subject = self.payload.get('issue', {}).get('title', None) - project_url = self.payload.get('repository', {}).get('homepage', None) + user_name = self.payload.get('user', {}).get('username', None) + return { + "number": self.payload.get('object_attributes', {}).get('iid', None), + "subject": self.payload.get('object_attributes', {}).get('title', None), + "url": self.payload.get('object_attributes', {}).get('url', None), + "user_id": None, + "user_name": user_name, + "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", user_name), + "description": self.replace_gitlab_references(project_url, description), + } - gitlab_url = os.path.join(project_url, "issues", str(number)) - gitlab_user_name = self.payload.get('user', {}).get('username', None) - gitlab_user_url = os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", gitlab_user_name) +class IssueCommentEventHook(BaseGitLabEventHook, BaseIssueCommentEventHook): + def ignore(self): + return self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue" + + def get_data(self): comment_message = self.payload.get('object_attributes', {}).get('note', None) - comment_message = replace_gitlab_references(project_url, comment_message) + project_url = self.payload.get('repository', {}).get('homepage', None) + number = self.payload.get('issue', {}).get('iid', None) + user_name = self.payload.get('user', {}).get('username', None) + return { + "number": number, + "url": os.path.join(project_url, "issues", str(number)), + "user_id": None, + "user_name": user_name, + "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", user_name), + "comment_url": self.payload.get('object_attributes', {}).get('url', None), + "comment_message": self.replace_gitlab_references(project_url, comment_message), + } - user = get_gitlab_user(None) - if not all([comment_message, gitlab_url, project_url]): - raise ActionSyntaxException(_("Invalid issue comment information")) - - issues = Issue.objects.filter(external_reference=["gitlab", gitlab_url]) - tasks = Task.objects.filter(external_reference=["gitlab", gitlab_url]) - uss = UserStory.objects.filter(external_reference=["gitlab", gitlab_url]) - - for item in list(issues) + list(tasks) + list(uss): - if number and subject and gitlab_user_name and gitlab_user_url: - comment = _("Comment by [@{gitlab_user_name}]({gitlab_user_url} " - "\"See @{gitlab_user_name}'s GitLab profile\") " - "from GitLab.\nOrigin GitLab issue: [gl#{number} - {subject}]({gitlab_url} " - "\"Go to 'gl#{number} - {subject}'\")\n\n" - "{message}").format(gitlab_user_name=gitlab_user_name, - gitlab_user_url=gitlab_user_url, - number=number, - subject=subject, - gitlab_url=gitlab_url, - message=comment_message) - else: - comment = _("Comment From GitLab:\n\n{message}").format(message=comment_message) - - snapshot = take_snapshot(item, comment=comment, user=user) - send_notifications(item, history=snapshot) +class PushEventHook(BaseGitLabEventHook, BasePushEventHook): + def get_data(self): + result = [] + for commit in self.payload.get("commits", []): + user_name = commit.get('author', {}).get('name', None) + result.append({ + "user_id": None, + "user_name": user_name, + "user_url": None, + "commit_id": commit.get("id", None), + "commit_url": commit.get("url", None), + "commit_message": commit.get("message").strip(), + }) + return result diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py index cd4751fb..a31352ed 100644 --- a/taiga/hooks/gitlab/services.py +++ b/taiga/hooks/gitlab/services.py @@ -18,7 +18,6 @@ import uuid -from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.conf import settings @@ -41,18 +40,3 @@ def get_or_generate_config(project): url = "{}?project={}&key={}".format(url, project.id, g_config["secret"]) g_config["webhooks_url"] = url return g_config - - -def get_gitlab_user(user_email): - user = None - - if user_email: - try: - user = get_user_model().objects.get(email=user_email) - except get_user_model().DoesNotExist: - pass - - if user is None: - user = get_user_model().objects.get(is_system=True, username__startswith="gitlab") - - return user diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 0f74078e..90023b38 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -246,6 +246,13 @@ def test_push_event_issue_processing(client): new_status = f.IssueStatusFactory(project=creation_status.project) issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -271,6 +278,13 @@ def test_push_event_task_processing(client): new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -296,6 +310,13 @@ def test_push_event_user_story_processing(client): new_status = f.UserStoryStatusFactory(project=creation_status.project) user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -314,6 +335,108 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (issue.ref) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (task.ref) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (user_story.ref) } + ] + } + ] + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -322,6 +445,13 @@ def test_push_event_multiple_actions(client): issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -349,6 +479,13 @@ def test_push_event_processing_case_insensitive(client): new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -370,6 +507,13 @@ def test_push_event_processing_case_insensitive(client): def test_push_event_task_bad_processing_non_existing_ref(client): issue_status = f.IssueStatusFactory() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -393,6 +537,13 @@ def test_push_event_task_bad_processing_non_existing_ref(client): def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -417,6 +568,13 @@ def test_push_event_us_bad_processing_non_existing_status(client): def test_push_event_bad_processing_non_existing_status(client): issue = f.IssueFactory.create() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -686,8 +844,10 @@ def test_api_patch_project_modules(client): def test_replace_bitbucket_references(): - assert event_hooks.replace_bitbucket_references("project-url", "#2") == "[BitBucket#2](project-url/issues/2)" - assert event_hooks.replace_bitbucket_references("project-url", "#2 ") == "[BitBucket#2](project-url/issues/2) " - assert event_hooks.replace_bitbucket_references("project-url", " #2 ") == " [BitBucket#2](project-url/issues/2) " - assert event_hooks.replace_bitbucket_references("project-url", " #2") == " [BitBucket#2](project-url/issues/2)" - assert event_hooks.replace_bitbucket_references("project-url", "#test") == "#test" + ev_hook = event_hooks.BaseBitBucketEventHook + assert ev_hook.replace_bitbucket_references(None, "project-url", "#2") == "[BitBucket#2](project-url/issues/2)" + assert ev_hook.replace_bitbucket_references(None, "project-url", "#2 ") == "[BitBucket#2](project-url/issues/2) " + assert ev_hook.replace_bitbucket_references(None, "project-url", " #2 ") == " [BitBucket#2](project-url/issues/2) " + assert ev_hook.replace_bitbucket_references(None, "project-url", " #2") == " [BitBucket#2](project-url/issues/2)" + assert ev_hook.replace_bitbucket_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_bitbucket_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 4cdeec76..815aba49 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -172,6 +172,70 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (issue.ref)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (task.ref)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (user_story.ref)}, + ]} + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -454,7 +518,7 @@ def test_issues_event_bad_comment(client): take_snapshot(issue, user=issue.owner) payload = { - "action": "other", + "action": "created", "issue": {}, "comment": {}, "repository": { @@ -512,9 +576,10 @@ def test_api_patch_project_modules(client): 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", " #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) == "" + ev_hook = event_hooks.BaseGitHubEventHook + assert ev_hook.replace_github_references(None, "project-url", "#2") == "[GitHub#2](project-url/issues/2)" + assert ev_hook.replace_github_references(None, "project-url", "#2 ") == "[GitHub#2](project-url/issues/2) " + assert ev_hook.replace_github_references(None, "project-url", " #2 ") == " [GitHub#2](project-url/issues/2) " + assert ev_hook.replace_github_references(None, "project-url", " #2") == " [GitHub#2](project-url/issues/2)" + assert ev_hook.replace_github_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_github_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index f90f74b0..5208a939 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -18,6 +18,7 @@ # along with this program. If not, see . import pytest +from copy import deepcopy from unittest import mock @@ -41,6 +42,189 @@ from .. import factories as f pytestmark = pytest.mark.django_db +push_base_payload = { + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project": { + "name": "Diaspora", + "description": "", + "web_url": "http://example.com/mike/diaspora", + "avatar_url": None, + "git_ssh_url": "git@example.com:mike/diaspora.git", + "git_http_url": "http://example.com/mike/diaspora.git", + "namespace": "Mike", + "visibility_level": 0, + "path_with_namespace": "mike/diaspora", + "default_branch": "master", + "homepage": "http://example.com/mike/diaspora", + "url": "git@example.com:mike/diaspora.git", + "ssh_url": "git@example.com:mike/diaspora.git", + "http_url": "http://example.com/mike/diaspora.git" + }, + "repository": { + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url": "http://example.com/mike/diaspora.git", + "git_ssh_url": "git@example.com:mike/diaspora.git", + "visibility_level": 0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +} + +new_issue_base_payload = { + "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project": { + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlabhq/gitlab-test", + "avatar_url": None, + "git_ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "git_http_url": "http://example.com/gitlabhq/gitlab-test.git", + "namespace": "GitlabHQ", + "visibility_level": 20, + "path_with_namespace": "gitlabhq/gitlab-test", + "default_branch": "master", + "homepage": "http://example.com/gitlabhq/gitlab-test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "http_url": "http://example.com/gitlabhq/gitlab-test.git" + }, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 301, + "title": "New API: create/update/delete file", + "assignee_id": 51, + "author_id": 51, + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "position": 0, + "branch_name": None, + "description": "Create new API for manipulations with repository", + "milestone_id": None, + "state": "opened", + "iid": 23, + "url": "http://example.com/diaspora/issues/23", + "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } +} + +issue_comment_base_payload = { + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project": { + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlab-org/gitlab-test", + "avatar_url": None, + "git_ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "git_http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10, + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master", + "homepage": "http://example.com/gitlab-org/gitlab-test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git" + }, + "repository": { + "name": "diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": None, + "line_code": None, + "commit_id": "", + "noteable_id": 92, + "system": False, + "st_diff": None, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": None, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": None, + "description": "test", + "milestone_id": None, + "state": "closed", + "iid": 17 + } +} def test_bad_signature(client): project = f.ProjectFactory() @@ -90,8 +274,8 @@ def test_ok_empty_payload(client): url = reverse("gitlab-hook-list") url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") - data = {} - response = client.post(url,"null", content_type="application/json", REMOTE_ADDR="111.111.111.111") + response = client.post(url, "null", content_type="application/json", + REMOTE_ADDR="111.111.111.111") assert response.status_code == 204 @@ -108,8 +292,7 @@ def test_ok_signature_ip_in_network(client): url = reverse("gitlab-hook-list") url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") data = {"test:": "data"} - response = client.post(url, - json.dumps(data), + response = client.post(url, json.dumps(data), content_type="application/json", REMOTE_ADDR="111.111.111.112") @@ -243,9 +426,13 @@ def test_push_event_detected(client): project = f.ProjectFactory() url = reverse("gitlab-hook-list") url = "%s?project=%s" % (url, project.id) - data = {"commits": [ - {"message": "test message"}, - ]} + data = deepcopy(push_base_payload) + data["commits"] = [{ + "message": "test message", + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + data["total_commits_count"] = 1 GitLabViewSet._validate_signature = mock.Mock(return_value=True) @@ -265,12 +452,16 @@ def test_push_event_issue_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.IssueStatusFactory(project=creation_status.project) issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message - test TG-%s #%s ok - bye! - """ % (issue.ref, new_status.slug)}, - ]} + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s #%s ok + bye! + """ % (issue.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(issue.project, payload) ev_hook.process_event() @@ -285,12 +476,16 @@ def test_push_event_task_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok bye! - """ % (task.ref, new_status.slug)}, - ]} + """ % (task.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(task.project, payload) ev_hook.process_event() @@ -305,12 +500,16 @@ def test_push_event_user_story_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.UserStoryStatusFactory(project=creation_status.project) user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok bye! - """ % (user_story.ref, new_status.slug)}, - ]} + """ % (user_story.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(user_story.project, payload) @@ -320,6 +519,79 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (issue.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (task.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (user_story.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -327,13 +599,17 @@ def test_push_event_multiple_actions(client): new_status = f.IssueStatusFactory(project=creation_status.project) issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok test TG-%s #%s ok bye! - """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug)}, - ]} + """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook1 = event_hooks.PushEventHook(issue1.project, payload) ev_hook1.process_event() @@ -350,12 +626,16 @@ def test_push_event_processing_case_insensitive(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test tg-%s #%s ok bye! - """ % (task.ref, new_status.slug.upper())}, - ]} + """ % (task.ref, new_status.slug.upper()), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(task.project, payload) ev_hook.process_event() @@ -366,12 +646,16 @@ def test_push_event_processing_case_insensitive(client): def test_push_event_task_bad_processing_non_existing_ref(client): issue_status = f.IssueStatusFactory() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-6666666 #%s ok bye! - """ % (issue_status.slug)}, - ]} + """ % (issue_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(issue_status.project, payload) @@ -384,12 +668,16 @@ def test_push_event_task_bad_processing_non_existing_ref(client): def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #non-existing-slug ok bye! - """ % (user_story.ref)}, - ]} + """ % (user_story.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] @@ -403,12 +691,16 @@ def test_push_event_us_bad_processing_non_existing_status(client): def test_push_event_bad_processing_non_existing_status(client): issue = f.IssueFactory.create() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #non-existing-slug ok bye! - """ % (issue.ref)}, - ]} + """ % (issue.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] @@ -432,15 +724,12 @@ def test_issues_event_opened_issue(client): notify_policy.notify_level = NotifyLevel.all notify_policy.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "title": "test-title", - "description": "test-body", - "url": "http://gitlab.com/test/project/issues/11", - "action": "open", - }, - } + payload = deepcopy(new_issue_base_payload) + payload["object_attributes"]["title"] = "test-title" + payload["object_attributes"]["description"] = "test-body" + payload["object_attributes"]["url"] = "http://gitlab.com/test/project/issues/11" + payload["object_attributes"]["action"] = "open" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -459,15 +748,12 @@ def test_issues_event_other_than_opened_issue(client): issue.project.default_priority = issue.priority issue.project.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "title": "test-title", - "description": "test-body", - "url": "http://gitlab.com/test/project/issues/11", - "action": "update", - }, - } + payload = deepcopy(new_issue_base_payload) + payload["object_attributes"]["title"] = "test-title" + payload["object_attributes"]["description"] = "test-body" + payload["object_attributes"]["url"] = "http://gitlab.com/test/project/issues/11" + payload["object_attributes"]["action"] = "update" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -486,12 +772,12 @@ def test_issues_event_bad_issue(client): issue.project.default_priority = issue.priority issue.project.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "action": "open", - }, - } + payload = deepcopy(new_issue_base_payload) + del payload["object_attributes"]["title"] + del payload["object_attributes"]["description"] + del payload["object_attributes"]["url"] + payload["object_attributes"]["action"] = "open" + payload["repository"]["homepage"] = "test" mail.outbox = [] ev_hook = event_hooks.IssuesEventHook(issue.project, payload) @@ -518,23 +804,13 @@ def test_issue_comment_event_on_existing_issue_task_and_us(client): us = f.UserStoryFactory.create(external_reference=["gitlab", "http://gitlab.com/test/project/issues/11"], owner=project.owner, project=project) take_snapshot(us, user=user) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "11", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - "note": "Test body", - }, - "repository": { - "homepage": "http://gitlab.com/test/project", - }, - } - + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "11" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + payload["object_attributes"]["note"] = "Test body" + payload["repository"]["homepage"] = "http://gitlab.com/test/project" mail.outbox = [] @@ -568,22 +844,13 @@ def test_issue_comment_event_on_not_existing_issue_task_and_us(client): us = f.UserStoryFactory.create(project=issue.project, external_reference=["gitlab", "10"]) take_snapshot(us, user=us.owner) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "99999", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - "note": "test comment", - }, - "repository": { - "homepage": "test", - }, - } + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "99999" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + payload["object_attributes"]["note"] = "test comment" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -605,21 +872,14 @@ def test_issues_event_bad_comment(client): issue = f.IssueFactory.create(external_reference=["gitlab", "10"]) take_snapshot(issue, user=issue.owner) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "10", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - }, - "repository": { - "homepage": "test", - }, - } + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "10" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + del payload["object_attributes"]["note"] + payload["repository"]["homepage"] = "test" + ev_hook = event_hooks.IssueCommentEventHook(issue.project, payload) mail.outbox = [] @@ -671,9 +931,10 @@ def test_api_patch_project_modules(client): 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", " #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) == "" + ev_hook = event_hooks.BaseGitLabEventHook + assert ev_hook.replace_gitlab_references(None, "project-url", "#2") == "[GitLab#2](project-url/issues/2)" + assert ev_hook.replace_gitlab_references(None, "project-url", "#2 ") == "[GitLab#2](project-url/issues/2) " + assert ev_hook.replace_gitlab_references(None, "project-url", " #2 ") == " [GitLab#2](project-url/issues/2) " + assert ev_hook.replace_gitlab_references(None, "project-url", " #2") == " [GitLab#2](project-url/issues/2)" + assert ev_hook.replace_gitlab_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_gitlab_references(None, "project-url", None) == "" From c4ff9604352236424a98ce840f34b387132e1e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 21 Jul 2016 11:12:07 +0200 Subject: [PATCH 120/261] Add gogs integration inside taiga distribution --- settings/common.py | 2 + taiga/hooks/gogs/__init__.py | 0 taiga/hooks/gogs/api.py | 44 ++ taiga/hooks/gogs/event_hooks.py | 52 ++ taiga/hooks/gogs/migrations/0001_initial.py | 39 ++ taiga/hooks/gogs/migrations/__init__.py | 0 taiga/hooks/gogs/migrations/logo.png | Bin 0 -> 97926 bytes taiga/hooks/gogs/models.py | 1 + taiga/hooks/gogs/services.py | 37 ++ taiga/routers.py | 6 + tests/integration/test_hooks_gogs.py | 502 ++++++++++++++++++++ 11 files changed, 683 insertions(+) create mode 100644 taiga/hooks/gogs/__init__.py create mode 100644 taiga/hooks/gogs/api.py create mode 100644 taiga/hooks/gogs/event_hooks.py create mode 100644 taiga/hooks/gogs/migrations/0001_initial.py create mode 100644 taiga/hooks/gogs/migrations/__init__.py create mode 100644 taiga/hooks/gogs/migrations/logo.png create mode 100644 taiga/hooks/gogs/models.py create mode 100644 taiga/hooks/gogs/services.py create mode 100644 tests/integration/test_hooks_gogs.py diff --git a/settings/common.py b/settings/common.py index 66fbb67f..333d310a 100644 --- a/settings/common.py +++ b/settings/common.py @@ -313,6 +313,7 @@ INSTALLED_APPS = [ "taiga.hooks.github", "taiga.hooks.gitlab", "taiga.hooks.bitbucket", + "taiga.hooks.gogs", "taiga.webhooks", "djmail", @@ -506,6 +507,7 @@ PROJECT_MODULES_CONFIGURATORS = { "github": "taiga.hooks.github.services.get_or_generate_config", "gitlab": "taiga.hooks.gitlab.services.get_or_generate_config", "bitbucket": "taiga.hooks.bitbucket.services.get_or_generate_config", + "gogs": "taiga.hooks.gogs.services.get_or_generate_config", } BITBUCKET_VALID_ORIGIN_IPS = ["131.103.20.165", "131.103.20.166", "104.192.143.192/28", "104.192.143.208/28"] diff --git a/taiga/hooks/gogs/__init__.py b/taiga/hooks/gogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/hooks/gogs/api.py b/taiga/hooks/gogs/api.py new file mode 100644 index 00000000..ced551de --- /dev/null +++ b/taiga/hooks/gogs/api.py @@ -0,0 +1,44 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.hooks.api import BaseWebhookApiViewSet + +from . import event_hooks + + +class GogsViewSet(BaseWebhookApiViewSet): + event_hook_classes = { + "push": event_hooks.PushEventHook + } + + def _validate_signature(self, project, request): + payload = self._get_payload(request) + + if not hasattr(project, "modules_config"): + return False + + if project.modules_config.config is None: + return False + + secret = project.modules_config.config.get("gogs", {}).get("secret", None) + if secret is None: + return False + + return payload.get('secret', None) == secret + + def _get_event_name(self, request): + return "push" diff --git a/taiga/hooks/gogs/event_hooks.py b/taiga/hooks/gogs/event_hooks.py new file mode 100644 index 00000000..8e68b8db --- /dev/null +++ b/taiga/hooks/gogs/event_hooks.py @@ -0,0 +1,52 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import re +import os.path + +from taiga.hooks.event_hooks import BasePushEventHook + + +class BaseGogsEventHook(): + platform = "Gogs" + platform_slug = "gogs" + + def replace_gogs_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" + + template = "\g<1>[Gogs#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) + return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) + + +class PushEventHook(BaseGogsEventHook, BasePushEventHook): + def get_data(self): + result = [] + commits = self.payload.get("commits", []) + project_url = self.payload.get("repository", {}).get("url", None) + + for commit in filter(None, commits): + user_name = commit.get('author', {}).get('username', None) + result.append({ + "user_id": user_name, + "user_name": user_name, + "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), user_name), + "commit_id": commit.get("id", None), + "commit_url": commit.get("url", None), + "commit_message": commit.get("message", None), + }) + return result diff --git a/taiga/hooks/gogs/migrations/0001_initial.py b/taiga/hooks/gogs/migrations/0001_initial.py new file mode 100644 index 00000000..09ba6709 --- /dev/null +++ b/taiga/hooks/gogs/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.core.files import File + +import uuid +import os + +CUR_DIR = os.path.dirname(__file__) + + +def create_gogs_system_user(apps, schema_editor): + # We get the model from the versioned app registry; + # if we directly import it, it'll be the wrong version + User = apps.get_model("users", "User") + db_alias = schema_editor.connection.alias + random_hash = uuid.uuid4().hex + user = User.objects.using(db_alias).create( + username="gogs-{}".format(random_hash), + email="gogs-{}@taiga.io".format(random_hash), + full_name="Gogs", + is_active=False, + is_system=True, + bio="", + ) + f = open("{}/logo.png".format(CUR_DIR), "rb") + user.photo.save("logo.png", File(f)) + user.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0010_auto_20150414_0936') + ] + + operations = [ + migrations.RunPython(create_gogs_system_user), + ] diff --git a/taiga/hooks/gogs/migrations/__init__.py b/taiga/hooks/gogs/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/hooks/gogs/migrations/logo.png b/taiga/hooks/gogs/migrations/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..384a58d20f381e06132089a10c9d5b46e204acf9 GIT binary patch literal 97926 zcmb4pbyQSc-}V{0yCft;LRz|OKqRFE1q49}K|(rb0Fe|#x={%cBt$xfMkEC!B!)&> znt@^FT?5?Dd;6^KTkHE~t@&rKbN2pS@w@guXD0HV?j2GhMj`+JNbhQZ4FCWKzDKsP4-%Zl$w^FL9;V^y6 zjlhnLg~rh8^OHmoP2exFzmE<55E1qLY~KHa<5=VIN40|cA|$;t6?wBR=bAfFLp0-!}FqeYlG z98JEil4cHqz67;+#7k)rKxqIazgU$5KuH}0%}V7m1affzt6m%HMc|$QV8snt83CYK zCu#m5z$}rS7L*$guu(sa00Z{2K*`|!C^f)T7@)D$>W~H|1OVZ?Cbk+tc@5AqKtcQk zARqyR^&`S~0UUq8>f4PQKETT~fJS}KL~fU_lw$Q30QjVRk*njBRtq*G5OBjcF%i7Z zGjNxlk4EA#*5h;~!8YHtYqx{N$mjO^03bJ>mU1q1|J-MQtaM;NBDR{$QeYj1hvu-h zUPLVq7P~3|z`RG$4-~&pB_mW85BePS{^mB0{UhRB&*Ny@FXT#9KCn1mD3qnQQAi(9UNbd`tM! zdU_@94zh+Ajbs_&=(uZLXL^ffQJ=~!7Mvcle>F)_-J8+eWb-rVd~Mca0~!@4KA&tJOtQK`_TD8Z}g7h`JLRG0Mz#GTQ>lB1m+Yp z`d+EnO#lGk+~8Z~%1j5X4E!y4%&oZ7t;9$xiI*yz9c?NUDnw79%rC5XOF~q5UiFu= z+_DlHRbmlq(YJq<;7TUgW?V}q?@E69m>|2Ar!kliNBJ88^<(y55g_X*(;Kk_^l4!y z?5!F&RMDJlJrOke99r?>8X^W!Mr_6!EPL`Vu%yGy)$8MBzX7*Hkgpo=h$IGo(U%`3 zsZy=`D9QLz{XOx}6AzK}IJ(@7p%-5nv0v)9adRYm)cVR*Pl@PYu)Zx4ZmPk5V~i?}T8P3wLbXGZlPXg6Hh0|{th*I? zM#EIY>ch?kH`e%zGQ>0(ZzS~mdgAy6UpkSWjqh97Qr1%YlIRlC68oMRQI@rmTi%J` z0*}G>XPVcRDVD{S@t&sMRyO{SZZPxloe`yY+(VVm@2U;T^us?=i+QF?_lMHm)yn_; zK52x}p5Ff0q3pLuJ>^W&b={O9li}XAJHJzYV;^{vkiMb~?T)bHxt76z&tP}Wo1rXK zyWrYL^tEmyVM&pV)YUZdG#6vjTh+G$Xyd;VC21ycB{AI+GAjC1@M-oFvGKlfuTfml zBg4Q_bz@P(#UjGd*XdGKZytpB(d<-Zg-|Va|oysoBkspxN3kg|og* ztEbF?Lgz;3&rb^v?~s_{Rg!SxO_2;R$aRXlxyZ2ybx-Z;t6H=xspdaUdYm%0BQ&+%SAge8_-QC~U|Gs}LO*}(h zWc>DO#%jiR2E4}Vq0dA9>W|g6Pw#$NdRXvaySmXi?4k67d{g6x7M1Fsvp(M(Yxvwy zNLg5?o1j~qJ!aQlzFXZ>T~u?ee0c(IsL$=YkIqy&eHbngrtI|uFgGl zKO#C(E2LL2ML{rwscYt2(~_pgp_|3L-|%5pMrB6Z*2k%`ZHaA%?eOWGKCv9$><6Om zL+#&G=k%9{&f6!hhE`G;YH4(!lIny z?CseHmVJ^fHQ#D__1aysEyLidkQ?YHXg~B1pb&Z!WQQFYtQ>5Hb#~$xzz&O5l$m;Z z^ks5OIVQJGnN~S2t}ZRZ<}vEvmdXf4#W>QkcHg~`aL#;|9ItbbbAm9cUX9s z$t%TJ*9cR_A0jE@-an>Q-QnOPc&k*x*{Kp=w5iy}^e(SpKp2jU5zqgz5jpDwXouc{t%XKfg zi{3P9lWNao$Yj%{d$B#gEicm~68bj&jho6RcAbQ3yLG!2Td&@dr(s`EozBrULf4b< zdmJm@l;A!PAgWd=_-*u%XZ#la7rUn>wy^%l@6yS|*?j$5eU;y*-biNdpiOlm<0%`RO%D<&@&Z!0Izy{`YMY_fi^O#lxB)Pg%&cwGwrj?6z=jJmNuRE zwWzqG?#ZC-u7JIyI8m9-gUsJbvQ*W1Gpp@kng*=agCF@wq}z^OP~jSm&qh&!=a1 z&nk}s1{{~t0c-`nvmSK4#gSauijXPxBtj(yb9d7|r z2~y+NnOzGvT*1CeYsZA}-8!8 z7+at9L*AKUCC?O-d*G|NZ`9-PYb|6X!ESFbCMzi`Fh~$FZ8i12+JDt%0RD{JWv8CP zV}4i-9la{sC_7-jSL?C&Wont)q}C+8Ovdv0y2rX4Dq%#gyCJh7y|m8Uv2NS1smkB> zc#SDra$x45$#<6yJ@zwOT7IT!-_PZ^^Y{=>Hxu-N`+T6cVbYs>`*WjSqve6pmhz

4bt#er}8p{7C)rE@dwz0ror~|9rh=<67BNS6kQh<{lqWkw*vj z&$e2mysI zuo%$qi(7g=BM<5mu)>tVxBz8*!9mYi(Ao337iuaPSVjN^rNcmnBFQwsTxfq$HwH+5 z8gdZJ8I;V!hyi7A_aUwlI4|fex~fH06LmodX08<-)F8+izn+$ql9G~AAEX4%lTJV% zW5MwkqF!O<>Z3UXC@71b@1av#&VUd9nm^@4wuKz(PV?8D5_4R0_taGXx2`1!rK_m0 z$qx!jO3L_W2GmNF@#{Cy-JoM$1U+U+S^x}Amqb9Q|2XFATwHZIB#JfYVieK8XxXqU>E&A!p8Yu*y?%L0S=BcJro+(C;D&L z1!Bu+pTEk7(bv+Ne!5&y^d5#U6K=F4^iCo&8YAR|Co+&ZxYL9ld>5X;xLm`X3%vsh z0y-`Y;2?3qmnI5^WlqZQuj!jR;Gk0c0pouWp^Zvt>2Hzw*F^(M5O!DMf5g6@%LU~a zj_N^Iw4DF#I3a9j;Md>2(F5RE9G9GaWjq8Z;_u)6KY+#yB4sZ7mj%<1$5=I3T35Au zVruVCN0x!E>+cC+Jc2u|I9@koDm#!S=N)4kA%4qqqh-?bhUat|2tJ<5Bg~f zsZzsbXI!XZw}&R2GrQ_*$|5q1*3Y=naGcqYWCZm;4Vr#?mmC|0rF-vEAtQhyenlJQ zA8ZDFdP$+i^N;w@h(3=!7@m*m-zz?7$x@2@Yefj{A8fYLzw}4r{*nARc!61CW~iqw z!szy{H*^P7b`>R8D<-t)v^@S>G?h8qT+2&tiu5p!!@rQbaLMwY404!VLf@Vn|8LlC zbC$tZ8DW4P#a03rBp}hEf9|*ZOQQXosK;|DEs=jM2@!H7F4w+X(n0QG$+sAj-~Vru z;}8AYy=h2GTqL0Jui4$X&=3%kbe;BMn;WL_uomJd(?+t)`L}Fbw&R86q!ZAbuGFrn ze{l$-sJ-_4s$tUO{D268oW`A+ZmfTD{K6-a2pGwU_|V@}LzquY*e-J^LQL`ypm3O3 zLgxh6V5$GhVL}+aadeITuX;P`lLPT%cTwX2`PI&GwW1<*E%xuLT)I*X4V)%sl1iN7CUp4KKx;y}8ltrNNTyd?gh}H)w&`zD~Zf|;ORe-)4Z z&{Fe{$vH%oYX7124MY21Tbmoapu*$x(p{vVTH4OwELt{=`|P8^i+jQY1ZZdXRau#x1h!T$w2 z2%q_i=!@yokc0Tncq>;-7?5Ms-s?f@w_smwUl*+Epx^{ZI{T|uxFTAS2+2foWx~AK zkN8lHr*F{^tXaIPJ@i7`<_<*?B(v0mOOO{riXwLKoI&@mdemTN6kF+uUl01$9OX^) zMU!6e#f7#5yg^NW@r7=HqH(GI9!i12?lg&B#84iHH=c45vakc7vCVTYqwfkNcQ70j z{3gs{|7 z3ID6GfJg|z39jpxzsQFZPUDqawP;>i5kB2*R6DA zlPd<%qH~TP(P0-^{*~dT@~~zP+Os7r>gC1Ag}Wjl%(aA4=6@59?~MD8*2CXNv6a&Q z!W=;a{5NJWF5ob*^cUtluso1A`4(MXGk@D!Bx zikNTNMfgxLzkg%CTJhhQF;j{DjrnTGaBfGRocQ9D5RYAeqsb_MK(o$qTs{$8R@$LR zHci0%?n?WQ02}7Ggk6KRe|cP@j9=3y2a1lD&>C~fS8n6D1!(MZpSe~y(*uc}D%eTKzTrGY}@*J{LH zH2Y<(yK&f(Sa+^k3WLa<3w@rLjL5h;IYt=e_yHQ?Z_zh`@vja{7b|8UVZp;+(PUSp zUa8&wFNfzBmNSw2zblE}TpZ_zOB4bINxKqlBTQ|Ct+=RBJr8bxtQ39WE^tDm0LA@LFql!p zL>6^-qavKY^~f{$Eb4CX58>AI&T?CkpdanNetau6X0F?TDize1)bS#OxKH2h!ZcpaO{fx#m30i{;QrtpecsLk{A>p;E=pppRnMY)wslp*apUEpT2CZn$9Q^`>Mi2gN+9BwvH^ zPQmSN%jr0DfvAYkW=9<&DAQ~^obBGwhA}62XgnA`r9704V^dxv1#l%`E4JLV6Ipza z`Mh0f@mi<$g-SQ8#T4EzyIGT*;F*u7ZUnc}vQx&(jKISoNSV!bwTW#q*S@~y>_sd3 zzAj5)CNybTrxR;O-Ij7D+!>_T2~Ces<0&x@hStZu74Cj)=Iv#k z-Bznd3zj~&QMlZX@zmY60~l7vy;Vwc1I2CZUa%A0`JCr7VGts1F`=>iaBsquU@D2@ zx90rk)H|rr!?`o5cIubc1GB%m?6hAS(FR?Z>|NmRY^F9m>w?jrijUbg+t_18EU|yl zuv5lAyg5nM`pS6XwIc!Dz$FzFvN>{V5s)!HKwTzgit5jOh+D3 z!gVD%B38mLgK>r)=^ibXiWEwB1yFgO(o+`ITlDS=p0%>{ye$uwdokk>C+#d z`h&19P;U;kDZ!96M3#*O31Y2tk88D@Fp*qr;d)O)Zl<|go-bu{txZy1>7eWE(K9Cg zi;nHr@j^%@798v89_KW!DjElIs&<(>&uw8IQ#3q1s*{pe%^Exgd-uhqrsCVOYqRG% z4u#`eedmgX`=D7jejWSk%UMe!MC16ycZzM4{X>wGcj6Uou;=}AI8f&qxpNNqQ5Y$! z{@8`*SbJyS&Lt+Gaq^2$BEnriAdOBi{tk&tQZv@4g=qKiq?v7T^HYG^%PAC z=E~%`)T%c<^KwYB0im4^Ihg7q7LT*3?smxsFV77ZhElEFh-I2_p&J1^2_5KF^*=@g z6qsmwW`I+>vdzTYg8RO}BOk`1A)T+Hl+pS%JO)u7oWyVw#lew8$6Qb2Z{+LST>TOY z>rDiulc`SmWAP;-sW`Y9yqi)&-y;rPL zT{OqEL=nhv_Emm?~pbX3TZoC7Z@D>ZA}@Xn$q@8 zYnNCpliRXm+LeAlRZsHxOc$&HTk}BPbWh(1tP?~ekbfw5quq5uhYT4`iFK)A_OoX^ zmgjM%5gEfqXx&`=K7Ok@8z0FxJo#t&rBI$-ddjqPzlYgnKh)ZAis@U|)lRbGd~Nv# zT(-OFRx1kpGQw=`C&=zh6l@^MWiuk)`mWYt4Bt@`R5XQkv0tX`b$i3-CQ?dIxp^!R zHTit!m8!2kFt=wwh08QfF}c0jUAm08PtBic2Psu~0tyhau6TX2b^f^(GAH1n7 z?NZ)yFWuWMJR3+X=2OL&1{ABSi0|MLaer>N6w6pbpD8a6EL{3+d5}!8^HJq89zd`9 za@ZK1$a@p{98V_Z9gB9HqfZSsABf*}d>LZVK}88tSzboOnnq^9@qa{E4^!ODZyng~ zQry8oy2o{s#9CL&MV-06DIEk3<+gm0fi0wpB?|L;*KRu<^GO;f5U)vqt z^_+KY2gi@p!|)c)LH(;@=!3FG6Q$$xXbnDMvIimZD?@Y-vInhHLi3qc=0Hgdan|C+W`SfqzFhefIeK35`^=<6wl@120?0~&&@ zW&d_OQ=!)au{O~*6K%rP!bD?b7H=nizLNLEZY^RGwsGhS)Ju#4wHs?lv1q_nIgrvb zGTTSh5Ds>((&p4rco3`IS@~rpn|i)!upU7)ixRmoajK(LmW@wSSh=dhCz!7 z7pETdaWkCJQ2_LZJ3PzDTMMPS+yP_yEiq*GSV9;sk~zmXkUPC*c^4M(^(CEI_8?fs zb>Teq#7UdyK{%8BcTtLZ*Iy~2Io#i#bANO0Ucqk}`$%klTesYew6#n8P5+|YDgS_w zbGKYW6Ng?-&@n<2RoLd4>NCEfagKn@#}v?#U+is}-(*>vRwZLQWr90#^0!el4f3|K z!rkJTj>6qmz31nn%a-7D(_(=OKHjr(iOq?zU;Kf^LA{N&+a}qOiZDI=mBhh&7Q;KRmrE%qYVHqRP|S|= zwS2HdwkK#|7GA4FLy&@w(SLR{ZlgJZ@`pCRu@S^26B)Dq&I`l5R_xEKBt>_$mpi6Z z5A6luaVrpW*D+9UJddqedg=Oh-|g*w#iMk#b7tjJX#%3EsRH}dxi>dmzfmFIhwqO@ zr&_|p_U8=o4>s!dV_VHcEW3Y;P1>g({G|VJRERFUiG>MQ3N|P9rvh7v$)_7t`3~qA zvV;2Ax;7=q+L2~u@elY?6Z#fbgvN(`A%r5g`dvHM@T8brIkB^G%HriS%%I(I*t zKBC1*qbKOf>F^WJCb~{WSz=DWN59~&urm2v3f4%yisC*xS?u)G;$eo}$6sru1O1|U zQYx3xF%BuQ!Z>3nUBPX~g=Frb7PEb!M-_}d^?5FN`}7-;loa&y@l6xMZ0y_)OEBXj zGpwC;t%^X4&j}|r0$n-+AnswAv&q_JGaR}^XL1IA5;Nwq&`NIl29N8gVc&@y_^Y!} zCd;EiW&e=P{KDT-pDA1(C?dw0bq`Fu`4}$5xZcfxtb39NZ(5fRaQ-yrwcw1_>PHI;={~G8+ob|a=U`+Q& z+$$sIV>qOFW9RmcAB$8h*%;CHoa*2|HwNb$KPG{|V6s~&Mg)uoea)kP%IH(E`$Wrs zh^$ayQ>yYJB)jGkS-UnBn3TYIKbk9;k=_Rz*nQA93lA)16U~NCdkxtKNp@>}#^5D32RtQ~vy(uWNQ3xxhSMDD zmt{OiOUa8p_zr_Eo;$y3rN3X<&Rt3eqWx2z=2cQ1SPUzaux8QgdPwAvJ#0r@Lr{0h zAaf}I86Qq?dvH7;A*4N-+I*jj$UD2JHd1|Us2y?rRv z<9pJ6Jl?E7hn##FjT;qKa@0@4d%ZdHw+{JSW3Cwp3@$kQSwt~fIQUt1@}V%Db#T?z z*slj;zkJ7jg^m5{1o7jn)V-=6K)$YM$#L#dy_LNY8XU^_wIDrSoWtIS=FzX2eiP+J z4}Ta>E;t`_BXOx1vZ#l{t0eJ<^C>gBZ z#JP%4H984Kn;`Ot9aN|;;<44ZHejJe75{2@JnVSQyGnhJb>oKE2KXjg)XR&yb!#$W zs;;`ACQiyZ!k#};jEOK_@mvE}1q{Z87Q}xYsx)hd;o|8EyCzq8jh~v(O;k1K zh@|e-gJgbE#H&*^dfn)ZYbKpac0viw9xcE0iyIye`WfN*Oyu3HO-tmcGZS-)J%5;p z%<`wNZM)&?+V{avqCAL|NY8g?BpGak@fI0)(2{r&2J3$mUH0Z%99m9ixGB?3yz%&hY|jB>6dX#FFi9Q7fF;N}(Lbl!zaXPNu8z@$nLz2SFdtMNwj| zvFi>QV(fuYvyXb0yhT5x1*s6K1a{lJm#><_TcmiM@5*cY(%`?#p2Pk}ek*~fe3xK$J?!Q6p!=5%hZ-dQeDhg3udqCwAE^g+B|pC# zi3^^=L`pIuLmNf9S#PNX&U@@pJ>P^rF1L98d6%74bMA;G+HLXIv1f%U^K|x&$G@FJ z`l7}8u)sEiM0F(1Q2=ec?7oHp=10Ykz`Y%QDLm`MQq(clIo*m(Ib*w;5`&}ggo)=- zj?)o0$1qRGB6&Nc-Lb{tb;F>h2`ULWKA+vXrNAtVlSconnlwQa6j1ij;zfCmK&1>j zRY(ZYEe?*e57&VzTHIgHc@a6-eWI@XfBg984c=RxLI(J*HxPlM-bIWyya`2Tk1tyK zJtV$~t{DCbxrz2P$1#ngE_;rS)DTnVVg9i#^?SzQTh3QqHwsGWHzT*htX9r{BwJ=E zDlQoL_s>YCrs&EOfM)Pcp_59m;BlZ^XXUVg8=-lkmPa|+>ApJ~DAMua^L{}Vfpgh5 z!X<{IWqv}LJY(uX&<)_%Ab)i-)F5FN zx!9({2v-t%8`E&yVz4DR9SWK6!6|g+(R=zVDRcPds6P!?<}hF%alTBkE=}dKDlpNe zK^hH+#E8%kOK&AO5Xl%)yl5G@9nHaU_TCJz9m-*KTYnH&9jr=TYL^ zDeZ!cq|H^&K3sL~zLj~xOd6uT1a|e8SlFs1EU|15p^2gpd8H!wA@DY7XL~Y5-68vB z^QU)9JrV>C^dGLfBxCoIHnuZ(%`lvO_eXj1K_kR;v2alZ#Drsl&--g~Qo~28%@=cn z(_CepkLOQ9y~jLc%_X(Pk~wn*p;f;`3@Q57?o(i%$v$SG4c9>w*alX96@9Ibvx7bH z_VKU}#LMR;w&I4)S`+geLD+f=hox8UT@b_%zI##WwlBr@8A53kzs)B9q z64i;_SA$~P+E5;XE@V7axTlGH9fmVRcwqfo^p&q*rjxLpl~?ahZ|`Z-mk}5dz+tcx zA{y7KxLTXsFpkdG7I-;T35qFh9paom`Z<9Vt`;_*dbeB)w!TYLM>2Nno&3E{@n{10 zaUvEw$9t@nn46~wb$%a_t4~tUslmF~m85;*!h>Ps*KA@1bk;=6o^2Omjxy?UQOC2A z)~knAUSB3E?)RRHGugN{j4wV%Ki4?}Y14t;?{Xr{8JRv3xUwOa_c8yw&<9y{y+!vt zQN#iF#+CD)r{UE1_CR2cKH)Zf-+RswN24>Ak+2A{kh!7!6Bk+Xw$JCZ3mvNqoU4vX zVm72~i5**}p^7gcETcD=-&fFtnWwY;sd7Iu0aAY!v0b#hZ^P$_RX`t`m(4*l5;CkL z+jSr#JSc9y#UNUIpysT3e8wcO#mzzL^CGZQ8i(mVAB8SX&BndXcWK^k&nT6hnIt6d z;j00L^(<^TTz`ylCQ@?w^4TIHHaE_6*i*C1nB6|*kLE}|VYk)7g*sbRi}Z=U>J%*T zAkARu>%9=Q)~T&W3%^V;6*H z#4r`aEtloDi11WvpVyqmxa&KRO-bMyBh1%xp=`u~vS^BWO(XT4cUJh+xSBx^1fe>` zYjK{EJfBBLnU~W^*S%qz!SMq}?TgPNW!c}x-5-A#Ts#LD4d7QJWu9o6oG&VUR|)5} zILvuU;?5#9;?_rKdiLdMMT~x>BBcX!SbMUF>n|B1gxvNV5P3uv^P@0jq?(3W(;oWu zzIx@saAt_ulGKabR~x#qudjl>&VjxeyE_rJat|JcA{<}(X}WYko( zHg+DZ7VzVG<_UAiX7%YUEU-0$4G-OmUw0cSL=IOk{OQN6se@{({@Q36GVFm)6v-p; zfUb!Qv4#{gclWh^*|?QvlXw!Xu}I)2%iy!&@MQ0SPiOk#qG~9otc-~6#;oN=^=IHi zS-+n?4^8cIvRUo<(Nx{pc7yL+Fg^n#qqT=eqPgH56BBw13yZ!q(Qr~GiE_6^^;p?$ zDLO%OsV3@>SaF;>7OM@%XTChlxHdd1m5ekAxKMjj<%{~FxZ8wi0gh_18Aaz_6C}%T zu~ahbtQIHy*&W7&wuf8|rzxp3C|HEX`S+$H0=zMSmMJYN$`ggfcGa)P2h~pkw0VLP z%yWYc@SSVt@tRSGW$rVc<029g)#Eu+^`3lYHO|Ye_^eAQFywM6%n}wPpJEv#?E*QQ zN)vY+OYBw>-qk+O>I>swAPEal$HrkR88M@bZ_+OwZTbTBjtJTGy;KeUNI_XZ;8gl1 zCbg7#^7p{}oC04#GcEks?odRRYo1N*cafJYj~=r0-g7>&^Voq!jMglyhdDK!VyWxI z50exGRw}og3@L2{vevp~cF2*YGWwpb6-uvWy4U`GA3gEqsa9vUWJ1rMWg$q~O|9fV z+>kwP8lo_424ay+@c96a+YH4I(=W?RUg;(G?_B6Zgz>7XLnMqQ-yhWwtbpwv^Elh4 zuZV2r5N}aiaMwYC>LKWwlb^?XgJnCh9{QoeLp8S%skNW)BJ^w`|P$IleiUZAdY8voq?w9n7y# z9+JWS_VC*p9e{aR@(9s+GJHmkte?(i zFdyv|hvwxlnBk{Wm&oe2@34-hl@h5=u4v8j7<`U8Ti8GZ?bsUe&`6u!;=APO zA4!Ej-+{f1cZF6Z6@zWNyLp#h=&;CAueH52WCR-@#CkV=DCj|#E_NN)ZGP#k-Kw5C z3c^Y;5CM(RE&bX;0K$adV<&U1ZpA&yRGZzPtX4C_}R5<@Ol zpjQbjwci9Rf9E(I6ee1N*RyoG7-~nZXp%--XXI)iy-5nj1RP*8BiaqR}$ly z*@u%I$OzM?y3CW%CttHE-`si#b6%&9KTUwwMg>!k-{Uv3^FvJxWE}{Yk9xRyEL67Y z@57$et$!;1W?MW}Cr=ew?lgIOWgshhqdsrMA=`K5+9RL&u$1#@*!dJ}H`co`bVN__ z+<5RY)%ly5F;g|Dedc=+pUxHYTZ4hN4P-nQF5C_^a~2NbEsUEm0OckaRKkMh4%t<6 za6?Qu(G6$^GamGK34VN+@X=A_HQm|?l|$W4;yV7iNJ8tPm3ouej@4&M#yxQmq)iZN zKgVS2aHTJ;{bq`Jn_D-_9WlnhpD-7Aa~38jkehajAMWvp_cyMjJ7NON;T%3ARq^1Y z3wO$t^r$VKp)2(hEm5(b_1Tc9%ttWz=-o{h(8(TUp(!gfD+h=Z>o-9zHTA6<9MgH0-knLc0|J3Y}MDt0f&=-4s2d`fF=vE`QxOpPT#`IMn}0LUt*6Z%4}WpmLEoQKEpm1 zoCFmUM12H=1;VVx?P6@$H_e#DO*8i*i+RziR+@@-n_4EPmd1DOLwqJoP&K&GPrs0d zR0;L_l}}ip&A`tR&&n)g&+1>(>>awJOHKISUSqjAIXU@fSNz9y(l5^fj!hd@9S&*7 zW@iV?X7`dqPtP$C2|F!@Ei}N-w~O!d3BQx#N^J9^ew~q6$Ns(o2N6*4E`vF1&kkOX z3ciz*G4#W@M$U?6f=muqj?f8PUGcC;kCyg2B`sjPJ<|Qq=F#_k)r5XjNmWCsv;T4j z;U^XQ}^Jw<$mezI}jDi7Q;x(*3(R3uIYl{ z`?%ilKuMwIlZ6=Az@%M{j$DxIaea=@LS(?IEPS3@5z%u!g-@*F`X<Vj@RnLC)mhFVm$EPhHc~SU}*F7PA zKoC+ttq$K$Ww>7OxxuY0S2o_6wE9-vZWmMWo(y-UYn!rSxX3GNJ-PTCS-sff2)qJ z>Ehy02P@18I{k?Yy+@TPjMMMNR4GFC!!$}oP>15g6V~)$F|BXM+27vuPEy1-lr&Lr zgvNPf$s-Nq4HwQuKT>bS?T&B4!8_tsqp!EoW$)=9r6R6Py}P$u>Mo_bmgrz*Wi|Mr zFwz#37zMnt=Z{<*Lsz|LMX#5Jdx9e)J_6-17YX4mJO)G5&hQKGgt!@$-gu*0Ub=yc zG>iYdzUR+n$So%dVF{@TA4}qQIH_;&tdgEoibAw{zkSo_%y#st^$7O$@bDO1QSnnbYXIiZk%p7JbdA1<2f1I*2U6=6VhrW!A4B2U2w%g1YRAt94Es#Nm6x_X_nuLd6Eua>B03xO)5}aL+g^E6dn7ixu@J9AAEFZa$oK_UBjEv62V= z*AycQ;CAyGnfIg~N>1%h7z#Nz9KS2%_7J;}$t34=v@lcP#$x3T>y8P*NAcDyuZIyPA3ug$0exf#$E7}sMQ>S#1p*%`?%M1 zP1R@JT(>Ti*5k_Wvq}+z)643z)Q=>bgvLd?dDL1Jcp-T^Op`RW$cnP2n zEz!nFucdur^{CkrC0c;8Xe`|M zw)#{!ByM`aIRGYpdjB=)rc=b0m@^prYm|iZv}x7&o?M(z&~Ef}zqA$950>m-ocU_yL~oDsut1l%zY|!2fuX#g4ToNf6xFwSx*nbQ<6q_L#2jr z`zp$xwCyLy(x2DWX$(EC_&Adl(f9TDk7AL+GVT$j<4zM5g#`ATyVN?P`|?KxVqs>V zR`L>bIhe@|`R^B22E4mBCxluMx_AUW7}h%WBMzxbHgm2E!#qkl!JE&YqK|%!H+#W9 zd?Osu9H;r`9a`ebl1a}v`kcOooa8mF2hD{a3YQd(bWsh#L00jdf<5?Nj|Bus?!5vv zPBC`bzOMw#s)gm`8oS|vGaHX;a@k%Q{PfhrAyF`B~s%~Y{WO^09#47JyyJ5!IR5*kelVqrd9$? zAc|Xrj4yO%fA1*$Jd6U=Yl45a<)=K-ba8P}b=qi{Jrp$e zRwQh8m@y>kc8lyvV6a=U{=*7~?UtI=$jN-VaR+mnG2uds~EWXHV-`fiR_0XNvEox z7f(G$&fzQiKt*Xi9zKrYogsUpM|~2cmMxkhM5pI_-rZdGK@EefX9<2Uy}#Ul4;C@} z6OB+}9lDS5_O5ZUHw%i0VE#HfHOK`|X))Y`nFLj6*m}HtVrLTgJoTrE^5?HLHT#`H zLGd+BThBe~0;B1LR#aI}-*}DEL<2uw=s`m*wnxcB=HBaA);iliQDH54J;4H9F|_9z48PkqHPEVZd&-ArK=0FIh>+Z*k5BUY}HPlsuKa zXA>IA`AgRJ;L*H;5%AUK+3zU6(m7c;f=m%PPrk7+;l+&FWktt7*=xC9UOqNHFugTe zB1X*?xpBoBe_aHZ&tB&;0 z2YSrDOYw5Y)9#*?I;>|C-BMd$M+$^vjevlB4Kd|-{%n)fy`7+aMaW?>?DUk}1@Fc3 zzLY~ORtteJXo;8B%Lt&*2U$JDh1%BvLyv;AA05^xRUqG0@{LHQwkS=A6#V8v)5jOR zu=q9WO*!cb6`-Su<4tE^~AVttpdLia0WNZN&{Z z9m&aFkQ!E7mnXclXhrf!1kwWAPhi?c&HT(c>D~csma(9p_+AU~?DR&ypk(`i1;wZ% z?n|unaySr0sd;?5la16vF?T_2%1cG`$5Y?$K`7IrzJ14iIqRcR^dm>ttiYR&0Y{sJ z{KxE+2{!^pRpW_6XA$Z{h17|8b?tL3Z0y}s)TR$Q$93rXn!qvaYbVl!?enQb?l z_?w^fAJ3#R4%TT2s{niGzytSn8~+fwuGfy;qrud=Qo&Qs4X-7ecV9bAe#w{Vu3h|7 zHy9Wj7j*Dp1VUwr3q8;Fd3EtdafLPqe+<=L6XbUuu5jFs#`Q{ORc(=~*PM1zgQmzU z92V&n0c@CJ6z@Gkv{IF)sQHqrvr2HhP_pL!+@AgQm6Ni`IiG;2jOM4}@Ox6`XRmjO zbFq|WOsk!))y(?w<(y5gp0v?7D^-|gR*cJRmbagTLr(BxrB_lzWhr+Lueq^J-%E|) z6`&@d8{BUq4_aYD@4ntaz97!$Lw#88P$XT#6yb0Ri_i$OFZ~elO+kU(aHo!9XJXk0 zSNt@CaO`0hzSmfHHIC-&x{eg{%191MtOgz=YnSb7+nEDvXvnzOp?z1g@UxIM*aQdC zZY^qnBv>r@i4%vc+tTECz!no6v22sLO_-Cajr=hR!JPWR;7{ydT_qyp9_dOrm#_V9k>rs^-(5WqX^IgM07D z10(L?p58OnLFb&_{kSnaGeU1x`9i-W^G_kK1p*$J(g%CEZvyz&=7^YXgN4U+akm6ngFJSAc27I_5wG06x*xOsu#M})R^95{dY%p(2Nk~aWn>nSVn3GpB1q{q4(ynU~ZSFBfNCQd!i-<2Z9hGTpUeK8EJ-{UJI*5H=#Ef}CTl z|A(lnaEo$%+Pkzg(wz!|w3HyAfS{xxAuS-?-Ai|)bcY->P zmLPyIihs}xQd|N$Pa`OMPUthPX@u@;@Id*fgq1DYREK)gw@lzP;*NA}y^p8|GcWO{ zTKXJpynp(`2dRd^KBFwU_;9!xV3e1}z$SIa(vnCy9N!sW^iS^nDtr;Y#Wh1m;7caP z1o%frvlt0InZPAX<1;YtsawG8C?#TXVQY<`fwya0m7MTjU*o(6ik4*V|EwF{ts@>= z=F~`R{L)V=MiO6X5@R& zAn{R_s`K2(r+!?HtRt#N^4srTdIA`VLn_qR4s&ys@2&SpTw@GC*X)W$Mi<}FXP zms(foLaQJ2P_L8fv`M2HMD#7`M3kK8J|@*^nV`pCT*;>`cuTb;?v=IDn+A(VmZ+;8 z@SI?OA0{}WO0h-W=>29h&0gZoVH+xMF z`UDc>c3lywZV;2COUg47C;mhx99gqA+&b6#IR)Xqx)Y33gtYgidn`Zq*2%rQkZ;I?}*dtLH(@#3~k(**=75cb~<+?r$tQ(k|IwTR8o&(0)t=9ABt39^y zm{`s^Ve|G(2<*KCZaUvWDO0+o-HNpzH{k_#+_c)JbV+sEg3vUs0|S?K^5y#v_xf>;)&%@CZb|13kj#*$9F@1D=K5C&U zIjf~|mU#*7`|L?k=Mn(;z{rYOQ;n-3; zU3RP9yYw#4B0I@(kAb*ykbUtBU-POiDv^v|NGr z0Q7_%mORVf{p-s0Q`lehf@b7-SVg)X@~SApP3*=b5d;_Iujw^5UYu00tk!+^8q)t@ zt@4d$qF6o*)^R9|Y`lGY2470AReigQiJ9NX((NGPu;f|!ze@%zz-%Jhp>Ad!!A7gR zLVLt_@&2^1Q#st@9{-k*f(r&`!jAB)XwoP;eF1y!t>D?yF^3<^Bc1XJPWXCMnoQQb z82Vja(}N=axvK)HV#nUQc2ccRFX{CAgttO|j~t4L=Kq7HH{$oog1{x-n`^Rh5&Kf& z;IV4pMhnMG->XOe2Hj$PXb(~|+N(pgdQA_Ok>maarJ|7#iZPbkn$eL#f;om|0jGQ& zahLe^c9ZGZo!MTP%C4<1sU3Vy74@O~Tlw!gfUOf+ChZ@>?{Dzqqy=RU8>#_c zH|QwQl&a0%NZF8_ds#|Uxe>|({xcR-#`mvt$0ELx<0ap1DUNVMD->j!8@<8>eIA~P zZZrq!$(_LCIR^YPnsj3bj?0}3n$V9d8n(7~woDKoF!MVN8oZ_lt2prghPVQHl)cW| zi{R9Rh951Pb1mDR-dfqhCtQ=xj+-md=|0<;I0H*R5p69O7@@X8q|S6DMT(8xfU_Y(H?@^9u|h$L~412?m=5#j^BamSwI z(7r}VNn2ED~`*v5(4e6lYktt#) z@$o+sTR#A27T+HhrY2_pU^;t97ZZ67K7-*H=W`2Y6q)y|e_Oe?!_kzX>O^cZfzbqX8$*1=tgs|{a;E|2I&aKpz7w zC5_Z@{-uM|PsMkMS)g4BiZd)|TLW?-{Me|FK*3-du^kzIQoDB=N1%nYF`v zo-_FiF#eolooFTYCsL$1njGzKfSO?yt-4dJj+DKiI2T0f8jPy2kU`b{Wa@x31fAa1 z_#tB&Qfhj7emP`H`4Y`3+OP{@MvJ&MwIW5TuEsNkb1L}$$Xk>Gh}KsS_vy3`H;gUj67?%BfQe4GO_Wzz9h%8wB)vhvMOG;$v=V>LG9UVXL1nQIWfhYTU z?)c0u6_r;2Q=5VKFOnpXRCpI$3kMhXy-j+P*Yw~lZt!0VI1@j$U{q$7f@@UujWDRY z%yF()+zpV&P1c<4KOmPECUk{d>TYB_zPg|{ zp*W2-^{xM|>d&^6oV-DhTanJoT04mFdwKbhJ$QXn)Aa1DoT@63dDxW(1kp$BbLEcM zTpiM5PJJfA|MumNUvCMRx@p^k)xcOvwkcR2=%1rjCo*dWo(vSYWfu}hC~!^T14v3E z_6Ie$el4}NA=&uN{GjuK3Fp8fNH)UWVBszmj%$680qbKA66&*dzkhVJRS?ayxnxuIDAmc=d3g%k8yXsF zIry1*gV>V-KVi@6jw0u}nM&>K@0b1dw5stod@F409+7S$scP<99=2#6AmvHo6{l9;3-5zxzZKe(` z>T~uY*>l$8Q8)2lYR#UY&WKQse+1F!M3f*w1osauu)u8cY(Ku(dQ#B7eGrUYD9yTs z5|`Ce+@aT-8dK&u{Ugs%34QxGO+u0$bVI9J`7vm=(Kz_@p{n*#l~iufmQn?JIJ)uw zmhyp?ifVlY>MvrE?M4B9bG_UX8;R`Wi)#bt!lL8^3JV)lq+s%ly^Nid*19p4%5U9i zS10tk!CX|e;^@Bb|BDvF3z?!fA9%>d5J$~l)m?}*yUTu5>W;EY!k+MS-k7)_FYvQg z!Rm4!Vp!uPlJp!p+?K!_@1J77;xhBK1lR;;3<)Vng^M_HVaO5G9>$mdrk zx>|Mz_5V-`{k!j|6$WxFSBHJCyvL~z*Yigb%A@1R!1vT8HxrglFYa-=T|cd|H6(?E z)#n!4Avf+HsMp{ML`3uQzVKjAasP1cIIHUMqPzeRN7|kn(x}j5MewdNZM-Jczd-o# zOrTORpH=tAy!DZk60j;s>aI{~lMw}DEW7ePaLtJ3X^k*Ko~KUVJy_oBv)I*L-@9z} z$zZ%yV;qW0?SVM4zPh2q``NG%c%73Xcz9q({?P7@fY%~ z_*AJy7dvU zv?9*?`QcNuvzN6X)VPCx-+UMH@`Y3ttuo1xel?1p4kLzuaL(xLK?1p+eG}|as`2n$ zDh(~DW@uLXE#aIGIU0!-CUdB?EFl}LiQ;9-JL8dCNdwx{WH1;(263znS8LZ0{~Bfh zdbmW~&3EP0>>0It@;~~s*!eK)lTbtCEWDpo)$!qS-UU7`Vp_3d%6L!psKARH+S2fj zojIP}4!Z{7y)}0YX{@wgc6<=}fnTflbal7*ow1v-lkupS9A8Xl zRM=rj6bheTg$2E1W*Ju!1jud&B!Sj+o&CF;_cm;GzWmr`WSVT!7thC)MBReH!*CkY z=9|sv+g{4fZvG)U_^)k7%F%(Dd3(s83I=30{No80_d(>yW zwkM?YykRBUJZKE)fc9--tahJxiaThY^ZvNN43&Ni>BJNjBzp zqrpz$f@eA|?d$c52V>K+^Jt93^|w@m!JSm2xTDtlheZNf;WJn~D9cfFEEjc~t9sk8 ztZ*KXDQ7UxWs>C<%Y*X^ z)MSDD^XmFbHdyrR2fi$|5QSf=;$~rQ5NCaY=j|&iE7%YBrlzL&L`2iGvqHsdv5nWs zwEm|7Ab(q9c0W8yp(`gnszF}-EuyvfNcXLN3saLi{(RX7d)IZ6ZS3k6dW^*rVlr3h zj~$-at>pi?H3vZFto#`iyXZ}>6WF2`-h7rZ`YQ#In$+CVit55`bXb!VY%y?jye_%l zNq;`g1sKbj0#iDFsU_X&*RUIR#7I5O1QVeVne^JFS&BTLGPs+r*cdZEGQ}%g=p}w@ z&n_$#^YcnE`*5w@YKu9y>naYL9QQA9W{BKkqrD8xt3O@~*-O0gt@ZD2CwD1pwbj+p zMwJza%E)C`=rw6=Ur!&DR>2rPjAxufV+inHeE_bL54k$g>Cs{50K1!=lv61@z9CQ8Mzo|KKmm8pShNdWqGkhmWnxnb| z_d5Bp_t|gURT)(kr(nj=0o2|+ zJUj?qEw3T3nnuj)G>{#j+f)#9E|@g-eTg<8ZTAKiEhkXON<+pXa1i_-XC6;QXms8& z>NvkW_>GdiT}34ufz)T4h6sk7-jST7aq6_WpTxYmp2!xz9qmGlzPO}rK5ad^8F%nU z|DJ;8YT%(;MgP%N<>-143%dq;v0F_xV~{yh#O||@?qAH^-GVs7rFa+IF1tW0>b3-@ zJ8q^YOU##dklJIo!u}4GTyZr8kdBf4Bu7?u?kOu-bq~6WL=#HhW;+2e#`#Xg|;h_uZq`4y*OBez1|x z5R~lSkfkv+=I(6R4lD)brg;G&ME3Z~_(aLa$GrrayEf;@@Z4RgE|eY;j@`8bD>0P#XU`i^N_jiA z_8?+}yoWj?(}&L38C#MxNB3-)qQ|npfGSNY*&^!4RbO~@(#Fh;Is*RPvMu)@Vs|TO zsln6JW@~_sa=u!GO=ZhZ`$l$(X!?sQ(T#;3dfLa1;(A`r)_`5G?-g(7_}x5`nRddH z*XX7UCAz98ZB;cCszXP{FC&KtHj=D!y?t;Xm zj91UagjR%2^-uCR71^HpUX~eE+`2zfzDJ!bIh71H>e9U8o9$V*1#sC3Y^s0LsOq%r z#kelj>47c>F0($Cykkf8dw@I(vFe7$JmddMv|AK_mt^RQ{@TE9_ASh;t2p=>?(LF4 z-vLEHM)O%Op4X80>ZiJSk-@XeDp2;-)fH}RA?_F&6>V|a+BNK~cMiCE5pJ}7Gw&C` z_;4PVaoTqcLO|0L_FkpQaXO6bD$`PO{P#kJ=R`U$a|niSi+4d*VD_W?@;CmC=bJ$NvL{lYh)_S z&)HuZRqIajjrM)sN2)3@ zN}cNe9qhNWcJ`2Ge{ImIW_CaEmB1u19mfA3cMT(1n3_eUuG~X!$$|oMygJ;}0cZ7v zhte-8wj5Ab4!0w!{=Efq(VTb77P)@Hj|yf7@99i)^Q`+H27~7w9+JOdlt})rwv)iH z1&`ZzpB~YHuElG5Fr=4>CH7;qU35LM;=v*4YtsbE3d~N4;W;-oJO70eLhXrpQvTX` zklNyGzcAkUq+$L%l-D2AYAUHY)wahsxQ2`0{>k+VH$hh} zQ+>AO!-K!=yj{C!KvbpE50~%-v*LWLqtTxqa5H$f!8fTBWBKwuOFCPQ(!_!-aU#|x zfDv1mi|nsiXBxVf)ZtxBOj|!vk#O{L{{RQ>(l|2%+Y+N;ON-fb1-4#IZG(ZwdI$0v zgi1x-RiQTbq)=jzMmq=;T4~gkgU8#R@TfST^WM*&rIxvkjFJFLA%?1MQOGr-nmOUI z&O=MZsg#In(<7}xd3kDD`ft8wA-2!!xJGid=C(wOmLLcFr$0-YQggo4MaUMu7A+$0 zFu(--Wj}{d0Q7*V)te;I%dgqsZ|^4+*W>Lp|&f5H!V8Tzi}!F+TjW38uvWnzFLS#KxR0C##yoGld!MZf&5=M)`k>o<+jg@MVUiq4H;q)+8HZZ$u+Gj=#0qTl0eX+wHh6 z9rvou0k7h2;d_E0B%&3<17)%R_}=FFFDHyhb>ES@{5pR&{vdw@)5_c) zfB`r2(6xRe)Df8W+>aKWouQ`a4tC|2?58xEqd% zW~@oNs*8Kp>Ph(d1_N?^R5+5JO52AeIq1~T_bmf60A}^DTIJxArus1JMsVLbUv+HZ zuBvykd3rB_{3Bxv+Q6J+biubfIiZP}Dld2>R(p@cit#@(%lSJpWWRa)bJ7rF7-{s_ z!V!jH_RB5{ZbSDF$69%Dqo2iT)H&zS_GZ%%sk`1L7*W9|!wLiErLS%^^joH;%ZM_g z8stBPX)9}0z!&3(=mEI@qQhqnn1kw+%Ro#2Jon1c1A?3zaC2EV_Jv=t2rYna<|ch= zp(%yPcLRbSFJzC3?9&?i_VByKt?o_dUd>L5o(||@ax6PilH`gU^;PJ!_;AkmxPZJ) zCimek;2ksfQ2PamZ4cJmQyrT((J*ddrgh#p&sfQHO#pEs`bUH*iTA3LB| z#yU3d!vQvx6rjvSxzri=JD2a1o3wY#+!JV$>pvLfy4$=*tD*v*RU4zsgQI|)fQVrR zw=55|lPYoGmR3`s)*FKdY>pU*dE`GRVXaS)G4P@NQKz+r7=Riioj@}EkNx_wk1apkX zC7P|?l@jsWup{eMfUSaoPF=mFA#3UacC9~Ss^M2qwR3V_ML0!juB&o$mJp*kJcVi z(a?UTgc*0#ol=P`JL-y1@bi-gQrvszky>IWNt59ak#+o0o5Tw|jeT4r{4Gv2T#T_T zY1=7$ZsjMYn<#o7AAdk9?R0Y@dJ47gVF2i=54xGNi@VI!El+iYk7>?Xtk);?f9X){`Ibs2%wdw%d z((s;cRuRN85QQjI6`A`;x{)x-*s()4@!DTg23i7}{8USQ8S<3GY7;|{kWKmE5R^29 zilfG%#i4fm{!j*Bnp(!zec8JVHVTDIe`^fRVu1ANdy5J%N<9Ve-=i z@YSVceo1(^h0|2nGv%5pcl&99ToxAPEMpozC%(vYR(rlrd?b9#$^f;;eaguwa3^h3 zM`vms{NXMeNPHtlz_wU5HZfFk0gn|3^d0DxL?SZqM9mJ4e=cgvprg}Nv#=<0n&BJ2 zegQ_~0AoaaY9(O8Z;nzYxQ>6vB#^u)HFn#T=_r0Ee%mx>a^xz;eMC7zDDl?$$~u;P z@m}-^;sqt3^{}180zV6Mu3IUJQs~X!eXUKS^(b<(3Z<7>B2wu-3koGm0OAH1`R|`U zZZjCSx(csknt^<}hEUHOJfM=NAdKoj>8lswH)MgSnNv`@+V(E{HC0uMr0cE&*}1A? zhu;rBM$KPP;D)f05Ezmqnw#mE=B-G#agzhi1F*>^U%Ec7h5mG{H}Sl+o?m74;dKWJ zRlI#_g?NW0lJuuL=}pGM)%thP-gcoD31JIa#~n3lkZMBQioik40Uc)9+;Y9RKxNR; zLDSB{qC6ZZtj7bOQqUU)dTHEE%hds_TO4GIrqoO5-X3oE(VF`l`C$b#07>D6_pW)xejX%)0%Ek#h)Jw=*`~R9foXZ|xPW4E zfWAiSsIs-JAV8MWPwmoe%I}t%pkX`^I{P3T*X782f*0RQHOsxT^Sp62p5UhkIVyhD zUs&pPg;Y(VtQI zHJC1PGXqbL=N9`cN_Ry@w&;r-T`*A@fvoZi7}2NJJE$d=w4D`JfCA=a`OzY z{d6KqLSlt!V1AAS)!ASLM4RWA0r(6a2UF^?i|sFv8%MP65ZZ58fbL-p5y7MC=vV6@ z$A1L3hZ;4HZ&SFl20)<84#as%c=MGMXu$4TDj14A?Ohr7Ii_KB*YW0;f1IQ%(BMS3 zXY&W0{2qvfMfpK2{s~MSy&LEF5S{69pL$~)o=UHss_^lZ$Zvc=z*skF1|}0AM$%7l zAa|OoFGJ42cl)G7AZ5Y{%&O;gv6$UnZTkHrm^4Oto5NeupP2YbTR+QJZ}DVT54L@% zjm1d2J;k+$Ajm!3{g1zFLBrQV18b5fQj@%yDMIRJ^CXse2On1KCxhg{Bg)n zGzSEj198l-bDxWx`wrw@4^%*?sOZr0-cWB95h#jU^*zP(#p-{WLC(xFHB&M?s8;XZ z8A!<=LJ)%g{tdud2@aJdR0r!#^v>i3dk*L*uHO?hj66yOW zSXfx(%bZp=4r?c-!>8UDJwVBFO=3COL!Ey9n>n7|w1}RNo{-;M088=!nkjrhV(>bf z30wS|`uw%vHRBOfQT~ws_%bW+Jd^EPQV2s4UuQ=FSWNU)e;mCy`LcDkZ%AK#LsM!p zQXSY$gyt*7goAdK4#5UKPYu@({NC_7k%XjFqmmyZg`p1eG+?I-OO0F`6OcGK?Y2l= zJgY>jQ>cu}{m$r@cyT4KFkx66xbP0GYj~LSN5`EMf8y={Wz&mh%^49N;X$Y0+6eTh z9Mw)Wwp8AAAIVNe<7;<&DxEPMn)mDxUV$oRva*MI^;s^h?e%B@&jF8gA%HHt|C3Dy zQ~o)vJ0zR|B?Mb77OkZD-58ZvV&6X+uYG)bek%c%xTn*a9x1b4Esz7~;N?J*M=7Zo z!F)LrN#o=kWKll3n~WG~kuPInruTgM$y7)Z@7@>)51y%CgXV*3tkM2K3s7J^Y(Ha* zf!x@=@&(&0+ksX2&bMP-t3)-?Yca_Nn*^_x4aj{unQ%Ps()Vbtgwx*7&aJ9Ij~92) z0tVn{wTlvcg{1iSvHZe0&ojA>{hqwHVNMQ)y$UCjSRz*^No?Jr!O5Esw6H4$tXuwt zZ-Rjouv9WT{VExh-STYP;`p{rVM6yqeW6isT`7^oXm_7fFs@-K==7Fycn=CV67}?5a_sv>{C&IQ1opb2TDjrXj*~_^yX@$mz79 z@iU;B!n9|TQ}Qu%6wVoSUtx=oJ49X23=0C>pM?=2Banqv-TPVrKZWj=>B;R7BBG-G z`}=8Vdf%c`xFK$rAa!D$D7kq3PU+XlrnVA&{jwC`H6LJrFHpcsLJA;{M-v$_V+ZUO zjUl~gp%CeQJ*P{0{A=e3u7?Ro#8N=Mq5b1A@;V?*6($z- zH9B;ir-3yp#{9+Mvj%{MWp>=a;h7{<7Gr`=OLZhJz)+(-9!7b}EPfG#Wm8A*XXex8C4gVf?DlrIDaZeS>Ap1ioN4etfsV7jCya%pA^Gxosp{ zq*(_-r%(?ZxDn>dX^69^=p_3LATC|oUTaFpV&pr$y~uKM)#w=NhG*<{+=SwbLs4%B zDk&80ikqbR`{%GZuY7a%YW(gAF&jVLnS*!xr&ZS)wsRvQ>VH=AW6UuKx8O>b66bvb zXaa~!htRss&KYn1?JXIUWgeLcTh{U{-A+3oM}3nnlq*zYd4My^y@{>^**l zC&5XNyuS*^oJzS(K9(!%-YKfIRw&poiF&u-Q*UpJ9by%1Ho55Ri+ap4s1)3+URY} z?it_q(52X<8XF2!Ge>u*>FG6mx!hPR=vwbn1w9tM%{{gwWqWa>A|j5faExtBaq?c6 zJl?~AFvx-XmdUt$pI@CrK!-y>|6LJ%q+`2AaN7id1VTdq zUG+243mc1kIhahje!9(=ppJs!_wlo%$bJpL4J#3`l0*d{GGbU#jxM}_AZ{@2zIsr| zk{mz|kUeDnTI9M0lEg_nlXyR>fs)5%AkC5gRaVi03s9CoH%>AdtMwZN30U-f3o)^y z*>6C9xkUbkqL_@$Tt5SI3LIa3fFd+6cyyGZ2%-ARF~}CU<%A1o3LJb%_KJjL z@%}o>l;g&{Lx$L$*(BEK@0X2rE+Q3r556ldL&=1)unwr7L)!>9o@X8&-dEJ<=cLk_ zLQ+u16C|lmqZ(|g=f9pyArUKr=gX{ptQd!gE~Cge$<{rd5}#9N@7)Tv*%gp+EFufX z9-@w@ktdfWAz3W@hhIrYd|rBtzh7B;yT+AAWHHM7o50U+z{gN=26hKoaEnP9x9{`o zUr@d9j`3mmB4rHw%9VNg2HWsRzX4ubXbU-~-=#yoe+MHZxOpwMkHvUPy~VpKV#=HE zgEAVYlFBX@PXa zfGe1iq~q^v%$Je!n)qr!X|pgmad%28Lh)X*P%eHlXh>(0^$3$JC;(XuC;R6)2bygM zd@arI<|wl0jx0f!!6gvf9W2Gtv6V>eL$G%J!9oC6IP7Zm_F}?F8H~`p7}NW}mhx+T zI@}SWwI07*r4}7A+}RmN$47#_8TaffvrP}WFZy5)237^}V?q*2BEbvoPP#-Nj4u9$ zdjBTA9-2&qhL%^=b^CPC@g)|wyHCB5LfuIv%6P67uU^(T7Q--({@O2h`OHP|?lmc? zRk_`GG)^3?`P`=4n4vFKT`JxCmy2R`RjK+4d@!~-jUG8!*(>K&E4pq58*V*%nPpuQ z3S8-)?+y}z$@4pp|0JPSM80r);Kcx1fi1?k3~qDJB+mT5{+RS~ zROg5|uu{5Heq%&7%1(D&JHCqIC!A|swdJxw$6`Rf2#w5ktq>(QrK86e%uXPZn0P#r zGh9ut4%*Y0gVB}SO;JpDN?duYj)krI~Oz<~! zzP}TO)S4O}-*S^ZcmLxv+@i~DJV^Lz^h`rU;O?SA({E9qXMUMoy@u?8OPJ4RRuav##v*ZdKLJ4DV&*7x_5*Mbm zGT_gshD_IV#UX+k#Bkl0)>GN{;Y}TfYjvbD{Sa>_nOW4Bz@(v4Ib|m8pLI%-#8md2 z!F5vuQ>8Y%6X^_fA^_uAY>(i29t7|iK%D-h&FBGq1KcuvaSQea4mfXht@tF+{mED$ z&;=x@8wEUiVxw31WC!AN46T+Lp}IFj24C_{5v}S@do!5H@;6q%$^)0^W{fhMxCM}S z++x^b(D3pgg<ZU{x{J-!E%+7U$qXD+-Ov&I@y_06MO|u~o#Rq@@@1ZdBkrfFPRq|gW9O`3 zuUREF_9+#KhCT9+&)-|828f@ONyiGw{q@YXrCVc@;_Y0H1)MOogXlg>tZw}2FJ1NJ zQ?sBpy>n>t+UP6@0ETB=_lZ)lrsn*~^VAf$XIktj+1K?KC>680%&sRIxL^v7M+PmQ zxwzH5soXM-vATf(mV783FK(f%ldEm>xgUPE4y&|#f03(7c$P~2JZ1maoqMJl8hcBE z@un3$9Sh6(lvoLW5i@zvKQA7?kT4vT5Au__$pNyQywwzIy611D8zmk<^R=LEt|}yY z3{k8IG~7v|60O_tX5b|{(L>0j1vJlQafHn~m6M}ws3gJsCUT9x<^=dpotZdIY=X^t zF<|c{q&uw6KUd9jZ5*K_`r%h$eB@+y)m_uYuy6rvBIAP338GD0l#6|IAWcG&I3-qM zdWJ?D6{duyM5o6CNK+>PkU#oqY5hp(18(3mN(?i^6el2*sr%7L0r_^p?9}q4!8ZFi*yJzXoJC4E%%9`1cSo@T6fZAN!{tpDAA1sN6N8@ zi7&C?WEtC8wAqkHl4Y*ugUE)gBP3-r{>?8UViFQyw@_H##fdPan=ee1xOfKR&!?*@ zw1D9;Hr}0GkXP5t4KL;QO60wJt;Cr%x?ZNom$GUtG*RQ0#JxrDy)Kd?UFo6T*nS^f zoA=T$U|q2=%F_x+KS*7VbKvYup>M!*XR^xl(kWHxz$1pXu%jcsCh?@|u4(K>#iZ2! z5z}r}AWsn&g6n&i8_X;$kA1raIzkC~uV(-aPQX6JBemcJFR26bsB~)mOP?4^;&3bl z7&8i{<@&CV{EUs<5)EM?X@BXhn#3EF^T$^**gPwaN!CHpN+)Wv(E}uVL7}A zfi$9lw*CDqBO(+I?tfGH?h{c9dZ9oIRPT1E0i2JYed-%%fZ}Z_BSRn)^uE{!b?tJ0 zUKcN^mqGWZh*2?7M;|l5isL6Xwdej2zLYU3%1Tc0hdE32tVsVEm^5s!| z#bEb)LDs}cz_gHtZF-xsE=JM^yAZtkw%(6$M9fCvqJoLc&ZgXy?QDcl@`Lw%pBYw8 z`MnkLetLes@6VZIb`_Sf4rmFYXkxrYp@hmXbLyLU`=5-{2FqpZx-HnhkO6vg62WXo7P8OQ0-;5elP} zfEXG6>OtvWS@Ep_8vc(%t3OQbte$ii5OCZ8Lz(g{xqBpAQP9&!>>SG_G-**9yC}Em zqbjQEABY95u-qGZd$zabHq12>Ub5o5Bq~?NN|#+n2n5D$O~v+mBQ_Dl%TO4-atDz) z_|(+Z_Pa?x<`b~i4ldm%o2O@#bkO4}5QYtF$Dyv}_x7Cb-%YBs2sbu34U2?fWyKO$lg!m!j{T zrc+yX2Nhe2j=VH+LBHc8Cb4oN86wsZUwa{h7K8Q`FK(6_zzK->tge;=_=C9BwVrlb%8VF)aw+pwI1@Z$x1Ze znGx6K2||5qFfWbu?EEJ5l|OT^k%GwVXJqi~TuMPnNoG&ak_d(?T`KFuo!@SSg!gU~ zLyby7B*}StjrN0{w=3A()iX-KO%)lG%*2*QP1?+ zU!ix`wLL>&d!nSI*D+%VWBx48&wl4uXAF2vT_)T;8oRMGNEU;eA#Nd@u9RX+5Un3a zYqHe>8qO*X2jAEfFSTCl0B{g2i?D3ArLwi;-VpJRk4f2DIx3>x8Q4u#D~uC9s)4E) zXwWfus2XgEc*j)H-&FhsVAw*E$N2pxN_D64`rg56^|G9`rWq^p6_he(tJ-rCt#`vw z*>H_OPINcuTKm~8wj;|)zlM)tl7L`ma^8C`3DJ|XKlrL7i4?p5(cm%N$ ziq6Xej71I`OTRkp1se7iKmLWosu4-Pd<>NWqu@`nMa6i4;zcCTy=CC_oQZEatit?n zNWTpX&C|y?blg==ZV6Yh3l+)xw>rbMm^MPUSpH}%_ z`;n?79qvQ6y1uc8o!QK=_kQ4>k|rau`ig}khGp}`q{7Xwoc6oLVH7Tah5T#H&Nicj z9w*wui)B<-Tbq4zStK6d5S?k9H(jaPb%bS^ItMnzZ^L|K=pr_EC*)IAV@7#gv+y;Y zYEd_nv`c|ij-glRe&izT$`~kgOukvS>%bXf7SDmM%uy-KX4rYcacBp*43_& zYpL)p2g_71OI)x+Bqnj~I>t^oOCef-!VWIr`_Bg`Cz@IYHJ@f-aAEt|Hl8wdVYN9x z0WWgMq+F--dt>A0ogBg0k)`Cd;UJkfpG9?_gGoCb+L>K8uEo%;W3P?e$iyYrZIKOk z0%?*&3JQ08IUD-!p3P%-hssA#Z9xOfnb@^ZITBCDRNjZ9tR{#shn|r|T2?EJ;)K3? z`K3L=3w}p*m;xAbN)z;n@F*qvzDQSzIEz|llKQCU4 z6PD2$)18xJe@l4XD)ja{)^ymIbG$06C8 zs|?ve5fSyR5vDlhCqp!a7hSJE`=_R8jHsp)Dh^whoo0Esaho?{zI(Gx(`M{p5hdPEeUpcvgQ3T^t@Hrae5X8TnZ zT~@YeAT*}ug+acap$d|nu>dV#`0aBP72QWhfjIu=!S&D8IA_<4C~@4Dchmh`<E+0AI9i9N;)gJ~TxSz&66-*v3U{m7r>@Hpy|ro%FTQ<%`gw zWK>UKP{mOw!OX58e-_*9WYZ7`j{D_2J(2o1lDnsWnud&pMV%Ay{SF`-!)%SukN*ju z8UNJuAj^v-*@G_FOQ7uE58A^gyyn_KCR==aFD>cfWq4a#%k`j7#vb~Y|6|(A`P-3S;~~LZ-Y@h-jB=A&G1$WF4urSW;0Dh6<4_ z`#QhZGrd3G*YAIidpqa4u5+C;6@aEps{P^my?ze$CUJr|eWpU*S+z52SE!*#s`$5r zjHoj^jaMWdXLV&G#Uc(wX>bn%UZT(YHgH$}cB!T3@)XA{PEx@ zxHS`akA1bXsZ&7-6bQlQ4}J#fSGP1)X%Y=8i#4;Z+{ZjxFet^6g)4*~XEkpLrNtQ2 zXwJF=6mCB4RCWHmcgdHc3Y2yTjNaa&j4A-c-LD{D{*tH^H-@(0-AC0G82jVdzE2Pj1I zOqWYcyd-)_6MwAzT6_0u|MBBz-1q$4G}F*=fz5dH?(vx6$lULe$0dbp;q&`0X7v~9 zl2mjvUFte!HK$&ktz*c0f6sPL9<8)n`1#{y+g{fgfd*a%!!vFWt)u>Z3jVgDv6cD> zibvO>(|_OqH=uPGfP&lW58Lk#N`DNIg?q2^EVSotiQGK3_AO9$lJC-@xuihDImUwd zx98lxGnb9*=k&I(e3Pjr>sG%;4bQ)pqpI~%`C}Z}vu$`SL%s>;At{)_@F|eRXj_YI zRG{u&pf*DSMFBkYzIOl~6`i~=E&(11Q!-3H>E)H|mr*P)91U-rajqM&@4ry3_C^|G z{`6su7<=tB&(tr+K=)AneRqhuOcVVpcZVkfU`Lk>`u!_qE9XSnG}>J2;yu##E)4St zdRU*#k4{6$&%fC@$^8bvqtB%#Q6KyT+d{U%fxFAqF0=p6HeN}tA90J*26QRV>5Quv zb*ekp7`c6QbmZRZ7O$;2!EIj8j=M}`d4uO3JX!`5qV^AGdq3hV(0i(Zy zVRMZUb5^nyH&p;*!D-}s)(f4;=B)RQD&$Y$4N~TTYaT~R=({qTOsubIrUA^i?q444 z3qznsC)RI1hEc2tKRg{sB$0Y#z~_dSyrLOzPH6Ds)k|u;7Y}Kp>1y)&7)rPtwF~vZ zPq3ojHx4*&CSBgwHnI9^%lfvtCp$OdBLA{=ogiujFF|JxEf6obi7(utk&7Z)vZ|A` zt!BcuRn4d@IqO4GLxaG+bG*D@R-JKP%VZqmpPymh zLy#RdJPt2h+kD5Bi|MgKqdom{S;ar*+rhDuvMEPx*4YA#Jn)4*^{sqZF8eg)JI=3L zr?uOdYUhv19Y7Pl=CZ=S_DTQO)QA-JRAeLy&Utmim_`fK2Oqo$lSOi# z!2I~dP-pM2ul{C1pmRqYXx0QH z@4T?*iFZk#EVtmlx3cPcA3%1M!6XJ_w4EE|;@Dj%3QbR>oHjZDJi1^C*9b6mROMx_#J4;S>@wW? zHOm&5T86JEg}rAlwJj}0t}aDoRCZ{BoVOBAp=+ljcMjA<@7pPy@GUqKkVv`!qCV?z zA-C$%1)2QtW>}1QE?I&b(5l41=TCb)#&iH*d`npU4*Jl^fxStE%KbCVY{I~7a)Z=J zrhXtmgGvZ*p$fig$NfC5sf|0eWj*Y|?k!c}BE<(R|6-7rY}DdO(KaSu0PYY4IwPJ^ zaMxPB1Vsl_|4=cBDB>+IG5D2y(}^^Cdnjcrerw=~XPqnWbR&Mcu*ngB2G+LQsTIqn z1NvQl89r^j{fgdf zUVCtIUMK6`WzXm*6TyY~mWJ)w;av9~8a-Jb2(FE_+N=-JX#RU2%s-CQ zs2!j?x7DEzwK<*S189(AzzdDpf{F%7Om(`v!~Q}$`18JXv-2aRT4(ILr)Nu7b;A1W zBX>m1@I(?uGWFxchZ-UT)6rB)aEA4ky1^3Qo=_J)U&&tE?b7mWZ-iLB>zdHs(C{Rl zECyq;z+EPKFTKBTf?f9_j6(F|VrH5LVLivtATa=wIxBnNbhMAn+zHzK1kWzGLD^ql z+&A8%v;}M-k!svN4juwt4TIZk ziMwJ`@0M+N0P52~IT-L3%0Qx!op$>EqNaglMU9Zq#m6RwEa`>nm5UHbD-w#8*N zvW}ZQKCr&K!IhyPgYu-VE`^p)v+khVew9P;<;NFA-Y*)?NwcY@<731D$I5ra#|RC6h2_gbxz| zheZizzI30PAJgqF?F)Ke&gW6enqntM^18Ru;}=W=CWXyMre6KwYusG!9BVVzvwcNE zQk_q0-ea!2*L?^+68mO>ihbK_XUQ2F3 z3vEB}3`b0D*=0dybm3rn&GCapGJo)NEuJVb`utzz_RPkF2IXkUKcX9g69Ix*n)n20 zK$W(d*ZRduHge0y*j4f>^9<}S7(9-XR4)zc84wQH;cstAtX;t*{OP%{s_WwTDqJlh zg(s82m^=u`XFWuMceSPilqegyvL3&dMoje4sj{2$+ocjritKHut``q52eA+aMTilO z5{nZ4wh`Jt_HaN7lb7DK|0&y;TA8qW6%UwNqU7qo*z3b2CtWmwXU8fwhTkke;9N@I z-xj41gSlCL^;tgsQ5x+j8)AAY!rOK+?t6U^A#>9Kl@f)gf5nmSi0?lzCIT)>0sb-u zDj@qZ%5Y*}FYrJq!eY#Q^NYgjUw7n#haDv>KG}vlzM?sL+eIaY_Gp$HHP%uZU4Zd24Pj1EHA@O(v4=P0oi!p39p(Vzmzt_ms;5Nqcldes}Sn!I`NpH%>H zy1)_gb94hlL+Hd$sn}60->Im&z87nMcggaTNIe69$-C0k5Lx=9(A^>yLV#)=3rPx6 zhqBFIC$J|Rf12V^*mUtVKM_!Pi;q;b;pbb_)jXssh9}5e%$V0`-LYe!yHJiq%5mfc zfJt8zum`~0@@IJPt2O>R5Bs?Ln}qvpu&lC_An7ym)m5F$@V3p1lq0GSdEM(wb0=+e z855xE^JlMbjYUS5aUoi;<64*z`|HsL9Y#KtA6o8K5W)}$6TX1h8v{HpzlvNxv5u(X z_E-?8bwv7rb?Y`+ff1Gv9ZOD!Snz}`s9GgYWi+lEH|%2M(>&Z{3g4lPX|z=_@L2`y zS67VYKB?4G+e*;la73^>B3KX_(t<_Fs2CtrIeUA zEHmOPt&x8Szh<1h5W^GAU@Rn+PC0e~#lIpz@*+r)?pW3HIbj*V^T*l^iAXh332!0o z9*OLz)$uQVNf^FIEobXZnZ{ncDJ!#EVnOaLsA@%V&yQ!fJ&&EdCuJqLAy3K)r#NvD znJyx9?yjOH$%O?!EV1*#&b*>q0A}9rDWaU~QrRJWY*R`LBf{opp_RG8*{Sw66#{|r zEkFvE^5OMdkTxnBGx>w54k(UYoyz=mFWM8As;nz@#-U*f;=FhkpkfRFuC8F`hR3+? zYRhZ`M7}n$>zOov(2dkgeBXI2G4+7{?N43`%-J*xeX?~i_rHj54Q@Q60e^4maICZk zOCeZRn-J>eilulPVd-aJpom%O{(&;>g{qD150+$KcnCp_LhrM z@b{Nf(gyR3hf;LoBu1ur8dqI?1olwOTKOmDHDs|QQqLEVO_>Gm7|(ZKRpFZ3rwmr5 z%emlD%>*xO3bQFV=1!qZ_Mo(TmPuokZU^1wtB?ne{QOwdXWI*S)v4x(0+&uOwa}e6M+15s>Q-gtlemSr2?PsGkWyHg z?CvT5ciXO>ZC?WbK2Op^s;Nc1v)qXzNou>ax8b3dNblU2zU8K;{~|u4#ES=^76#=8xxu=>94jZCiVVjFI!j=Kb{j-YR~;vW5)%)kH$yZ&V5lqIdz##|2EoC-Iz35Rn82{QTpWU_^MtlWEX-4 z7W!ax50n2rs+F&dNEMuHaY0Z+|GVpVQMYDGQ(6(zdpfmm6k0v51=Peg_Vt^$>W&GLbY`$z@g<@H3M}wRg z0t)0-!Y`$cD5jnhyx?4(5;T{SV=u%b-OfGQ!Z5bk?6tkvi@<0;3fT!59|qj$gOyuB zZBvWv!XjT`PTv*k;aHpiL7Qg@zspb1*t@nv`O%%VQ0T}=;qh1^qT{-!8tf>zWhZ=PDTVI0^Eh`LtBFqZyR1e4nQSZnjx z;m?*EtA(RxW=2=-(3^G73W*D$_zAW}%%C``#v!Xpqp~8S%D6w`YhnW1DJL{(t<_%J zOsrzNm$k1fs`r?E5~NfG1kmA}eRW)aX6AqKwIj4jecc>ijHjZ61W0z!El& zsC<`jw|)8!&R6N&&Q)K)aKAYI;*-%tf?WF+w zupP+M@hKHhG=l!aFSY0WMo}^EUh8!csurzvNm;en9f#TMh5xo3+~3+Y*F3c$?z~~f zPF1|)iMtg8)l7&E->#Oz5(pN1tTCs$K&>h>k(zdd3p}#9=RiP%oMr;Id0)X4#+*2R zW38|{d44hGSiKp2{>SAIQFtP3rE#@GW}w}2j7+6v=chheeYA4lOK-c)eXD|R7p_{V zW!7P{@)@Vz-q=rx?HNCldfX`*>)XU&cur}gqYIB$Y*I|U(8Uj5rhc5bYI_1u{lC5d zf3O&gQsKkd;j^JDjXq*)m2P`MXqwRh^7nq<%~;F@yv+q}_f5&}zf-iMKJ|X{ODVRe zeNw)?t)^IXI9Y}h*ammHJCJ!XMs^i&8%Bk;q?CT`} z3ojoa>&12AsVyR)LxBB2LHE)ibNbQ7^slP8^|uSjZ@-rIEw2_8Z)_q^GE8;X!nG1{ zKJ7oUblZhZDX&go-)L$D6B!K8jt7TcPg?zwi)u|Ux27E-%)F4La0A*LKNJlFyKb;Q z>rm27#|lM2Rmk$%je%9{YGi!AQ{{6g=^BTkoSFsfp|4NgEG$Gim$_AWod8st*4m3w zdjX2m{QkTi(zIP^X0TqIrIdad&HA7lt@3#jq9lZ|pzw)C>Vtv+Fx|WnX@>~kQ3?J) z%=D3B;JFflWqD%&!0-Rnv@PxYw{8UrH;1Hp0d-Fd;M&b+j$>A2A~CD=mM?(-8=O)WK6I${2u)5Mb=A;08xTR^xkhc$iR? zK6d)kmKFZ=Mg3};b$jav=aB@&D%0Cn|49UF|5H-4WVzW-vtf(oELiKD~8&tn@w-dAUEAF969yAN-?V>MTwC zvaJ$DrHB4S38@kw5OS2qtg!{T{!mF!0Ff4yW1nNOi0@cMr&lI4{bWhLPQPXE}~AS%$u1>*ceQGjvqa03c%?;RLZ2W6!nb3kEm@q-e!%- z&fSAiL{d(#rV|&wRUe+Y6Z*^P#t$F?@SW;Bplc_H$EGzGgDQLQ{TO!o<-_Dz=B7?T z?@26`Mhhi?Hvs^s1vwNB89g!uNaS{i11!tRVA*n92RtgW1K-C;aPr1R3-N`B1C6pN z0l_MSJNujgS(ArKbb*KjZX~Bib(}C`#&DN3FeVAczu94FecCsG^@&?Zl`v7K`M9(x zu}D3h;pvM4OxZoQYwCcGT)8P(vf!hipoPl>hUK#r0N6@dXeVI;|9(?n_rG;Q05SKk zZIl}-52T?ehkl~FE0NXQgP@NS6QJlW=(^eEcm=ZC`DEY@qvVUTOPmtgghZ-&HDn8) z)aIcU%P|u{1#ky>EwdX-ZR5oQt-Ap+CwIS<$Xa}&H`l12qaGBtU}@NbK0y1@DW2l( z&EdfvQkBrnE+SK``xToX<_3}aQ?)m8cR=4lUjvvCfI-p#ahtVym|aGJDu&BZMsc%e zUy*XgPf3k`5J#Y8v=Rjurru)9+DcJ#Hv+VUm|vdlliEo=7cyVXvzVQ`61~&Du4k2!h~iz`h#C|zHnYi53cN( z>%}v2%;kqWXPBEh1@0>8N=b|Er_ri5fkeAUdnzPA+lt281{uTpgd~-JGoWkp(7zl4 zRqEJjx2a`}kSf8T*rzBKzaPP+5TC_jaD9+S)gaI%x(kPJ>)g%v+c~)KF2*3NB?~1{t)w?jbvH{ z1{{e6r|bEcetJ~J^N#bYs}Zxw_^oOhw7Iz_NdmGM5~=3&@2`izgS*0S(y3{v)1TGU zQhMvIAUM4DyC0rDmezGlB<+`;Q8R zJu2dc+OZ!BcpzO@q;4gJxI4o(7z>v+$b8I-6;PTrwGunPBVENMpCiS0D(XDb7SL$^ zHy~zv*LnvJc?#Cyp=8KiL`_}#hhxX0LsXdMwxEBakH1kXa4g*u^cLD6mHbkvTxuu?gXpvqGqwy5MW?_=wuZw3^)M z)@fL{^I|LisjLdXV2loEB+`zl+=nsZGaSa7n1NEM=U&wq+Ur=R2c_qKZ6P`4wbusa=HR&~fQ1ovfURnUj{ z(Gd6TD+aPWBvQx;o+kfgYgW; zsO}GMfctJeP;kiSn=%@Zbk9Sekn(I0bD`hOa3=MDzWZ1(xbe8dXu5Zc3AWBzxbx61 z%%vwk5xMYC0OTAMw&jDxnB~urW&8ZGR?k3dNZbQT(}^Qokj2Q@1G^h49|z`SjN;_I zr{(dTf?OU5{7nLCfFR1BSqGE~#m+b*_+t&hAl~KxTkJZf) zyGk)nT&F+3Mt^?ogyD?m(%ULj-EwHZU=|re%pq9+LP+50k4z02K8QaEf?tzCflIRt zlpnHcr5lA2J_#t59phmTXO@k3Zvy zIo|o)>I)E_aQIZ1AocVxIqa{mCoqR*LAqFCz42{oN-Fvj=dLC8U^X1xjI0p5Dz0?TEv3gJtFj4k?h0X z0@~I>nDmn>OudMo_9+ziAQFRtOf`IdJ6X743@*%{sT71RL(I?JO-QZ`qro39E^~vc z4}J%zP(N8Th_LQNu4xU{jlAV7C?l!Y3q%hatY?*b;pu2wKA6WI;0b+USgQ#fF?G>a zg?T>BrWm~a=?Ed9rE;h8zZLBPS=l${D2ZwcbJLZ8FImWq4-h&p@IhmCu*6hZ>e56M z%(pLk*zhT;?pJ94Ydb(9kw`rfAh8eTR+lH0GChAsCS<69Y5^jl(}D{W9D|OGkjIZ3 z;K@zruhaWODd|OOJ8#sMY;@ln;5t`#i9jF_4$FY*jKxgnpT3_VYK0)Q>EO&iJrpVj zFajgwsTOWjenYq1r#zoV2vCKg$_#ub331LJ_cBw2!@bM`6-Va&(u;Mlxl zTz;w+J1MloW&ZBxTgU1s(55@C)9Jb}J=F2?}+^veiG;1fI(W(O2^|>P1 zg3#?xm<#>(Xh<>Wq3nBx(Nfo<>7`qs^Xc)hgP7N9;UrSe1G(HyT7?fT&KO-O6v%D@ zFk>{p6kyZ(T@58BaAm1MtszD;cCh8zU{=nTBz*$S+I9`y6&uS72!sh7czqk(boYxE z!;-gBm2*V^pnnPq+tgsj7_FRY;WD>+hEmd(z zSbi5b7xunf-YJ7*kqNdD<5?t0aB=U(J(~YRohB8t6x4NWYcuO)!krW(j|^%@W{y;$ zQd`a+{9w6tmQ*umUlfq_H7^}+Yk{a7FQX+n&O|*B{Nk{c_EI;B~| z+VVFiS*uU^sVA>Uc0zttR7u+j71`Z=-qVP*oic%IMCjdms`;|v7M)iTj1fQG`lfK0 z3H#H)ifCBNy$0F)szJLMSLa@FlSjZVCffbWB5FIx(v?3Q({g1p@i7lF<&YuTCW)d5 zc)n>r4)V{7AUM?XKJ@Q|9jr4|o-WBU=Q>)@#WMXA#1_**Byq#&}BZ z)F#c}`uTd%&ushP$S^a>WVF3*@AB)dy4d|11UfHMx9~XFFDK`xr4_IGA^F~(wDW~U)B7OA7Ws)uNVmOY$K(ecm8?n;*3$(KY9Gp z1StN8sMozjm`9M-e;%nzv|qSpp~U5Ai(8idTVZ1w+4d$H5j1IIk$iCI;T`K)W4{nn z#Pf-mIF#1zDmUX$;t1W|lq@loa;RE_WyEgP8w|Ul7{#z!SVgFX^6K;1>a{EtuoOVd z*(Y9%!nX#yJVWK_IYRmhy}0ShElb;$E>#?g&(FA<^Jfv!GWj;AhphyFv=GSOE#VHR zG+_Y<6aATY_&?a}!?6^4aelw&PNfI`-ED~>+!vp7EkwK#YjXil?d9EnoVduho{`aj zly(+N%stNOOYhOx?B|+w4C=J9~5kz9-XGq{UXFKNn^MF+`Ds?P_BS=o7!5<&4 zY3bF&>wU5c`y6KDh5;W@)Y5r`^xia{L;D4%T;}VEg#-uJb+rgLv`=Y)gr9o(r~@&+>Q_<|M+(9e^qP##ES3m;bI`ZlkXRvO3I1OzOK0tA1YL&W+2UfE(4m8X`i??UD5B z*2`=EKa?Clo{7y-5_^Z*g^|UQa+GdR8^RthjUZqrECjuFgf16>g>n&=N5hX|b4d&2 zk6RFZX8l{vMh~J`wV@IJfYJ#-t{p5fQg$=Z1lE}nC{Mf?W!_=}KT4VQrqTlCF?eE=pFNCOx9LnKplxq@G2F@GXH900Mvgf~ zh?cI$`kHO$)|a61jMfmFeHUGCU46}PxNR;eAR889=l(i|>c4~r@+380_Yv|jD0}`2 z!n-eh=KcqT4|qcgM;~#ZaF6LF`li~aH_hUOHrn^jXl?Yn3|h!^%KW6UGE9dIBu_`X zZ#82ih%b&fx1MJHX+>FL47@u_b;FdNZY)N#eeg2QkKPRMyJs;yxv0!`5wLfKFCCG5 zop|PfJ{(=Pg<@>9XD;_d_GPPqpt}|dm?hR!gt*H_;0Er9GjIH)6JTNX4ek1LaFGzDWgtk4=8`<9@KGvwoU9v0-8|af3-2a6OoRa7q7fM;=rgj(GW414? zWsOenX2}Be`66EWS0Tw=ToW9MIW)lbtwOa1C;$3PF0h9-7h=7vcD>{+M!o6P+4zG_ zX1#Ct+odl-TV0=5i+-us+D@T^kDqh5qu*thg-5)r@moZ>i-`pv@l*xmxfL$mjGf(@ zy^LKXZZj-_&Ida0i?m9D;qfmAWk90Qx(5=Mr=2kt_Qj~thx?x4s3K+tcq>J1{Pg0$ zjBMC%`-okb?-H=53mR!9OY){;B~PQe3fYm3?^QrZzk;z9oD6 zwOrtx^SSGFhD>=n+$^lP|v%@ny*X%rR&MKv~O9k^T7SnH^BGbEneEMX@bXJHB?~`Q3q-pF#o1tP zI0Y{|`fkW*5B%ImOgmDT@UN3TI$FCW_aVD^0>}WD$k3CY43XM|jx9V6Ngg7P7~H@|gw<^jCOtFewG|D>$M_kx~OP8+XGV*OJpT~=93teK2ZolGQQ4j#cC%HdRpW)~Z zTef`LmxV}MRxH5h9U|WeQqAYR`atRSbGT^6wBvDV(x~ICRU=QOHiqQJ|X@Et=0s5d#=JfzqRT7)}|MR&roZBlh-A~ben~Z zDGzFjMNvG_n+P&TaoK}ERiIYa5gbU~+JKCHk#vI~a2XA?I6_BMg&d*pKOLcn-z^at zF-%7OO=M6F8Bp-Q$>(}aO+sZOt{EizvyG7NKFGn@IOsJGYUegycVVp=JNQ#_M$@fX zY3@Cg;+hG~cTF&|--6xxLb(~>NMh!Olr|b>j0#=?y~QB<)l1(}l!N)E)}XkPFN%TD zVvP&+BV7e_KJ~hQ;QqA-WjcwO8>{7F9*C5`27s*!bxhj5zWK!1IaP}?Cn3yPZF`rO z3V}%*Nm?5_U(OuaM@d>adO2s{M--8y8@*pWuJaSymCe4^QhSV~yCeq+qu|+oH=SZ` z)XoRI?)9c)-)WAJEc>VMSEup6G`z*51!cr+kumc^bPDYmnz^J2H*0btGf*B=;0U?b z6;*LPc3$nmP3KRAsUQsZIs412?0bl@Dr4;YQ_bzE04oSEMFA;CPZZESRG3Gu+o(Vv z3q^<*cX!%>q=dl6AN6=bCO^_0{Gkj;Ia`C1PX|GJEZGlp1UN~T`L9&8TnhkYH$(nY z)(z~AYaUUG(VjSwb;FPhK_LvGlOP<$m3R#3e??Jg{;&yvuDhyGub&mdIGO9GwKrK- zi?ALx&U;*=q#=sDpuPc@I)iNV0bsVejqC)dN`NQGqyTVH1^!$NPDlURlkkAj6gKoB z)lY?b^kxv-m(7bD(rP=-zjp^hsk#2%TBfAG#69oX$!iv}NFeyj!D7ar?2CaO4=P07 zry57!{bkB1;>%vEoBBeuPX+bmg8$CBO=MZ6zz(WDU671eLW7(l>*y3ReRJTGGG^LV zg}QrCan5(`b`^1-6tZQ7+uQyYbJpE@(JfvqHw_}6<I2^4`n}31qb6Gr-2JL$U{L}g_s`!PVjdyJy9|L_bpi*j`g1Hw-KU%_+ZKV_FA?V*A4~|S)mcXgj~%LVU_s5Nt#gx!%Jz8Bcv-mD8tcKgGjlRL4U2InlJl4DMW-i z)H&U32fuv)d^bv}njR`yVmfdBKI=nuLp%{()PD7#ru9!pq!9nOSqfx(ad}o3iqLiT z`F$y|16oaXaMGE3OWzf*&eHG}64quOZW&10Fw9@A->T6ZMx+Z*MyV;he3BwW-!+pu zBl!E0BNVaf5h*sLgHQ47`RRC%A&0cNWFS(I#2z%zL9&eLTvl{NQ}R$@6#Vupm02Ug zbbC?^e8cTVEZyj>u`BC&fTz zq;%-zh#Hb0ckB90w&Rd6Fx{CZ5D1wc7C`Gi`Pahh#6l;ZsxZq$5Q{`^Cbcil4t80e zVPmq|r)hW#VfClaaWG@_gGv_-HbcOxUqkjekrVGp05nIzxpr{v$Ib6gM?Zg=JE$;# zo4%^Grl>s?yql44UixW|4yTnTbAmhj_s^m+kR7%4I}YjOUA4gy3rygdDER-BYpbxw zxn*g^7-5Th3q8t0C_<;JTB2D4w(Bo@@O{@e=47u0f|)!D81XY)4(9q-yNZD5b&Pty z++9BvW=G?O*fq*m9F=0R5_~!53kOYJ@&|#gzie)JSRi42Yo{P*Wjg>PX^l)2riMr# z5LW$yhVHd@i8Z`E-W>TOsgZ9aD0o|%Lh-XRNUtw0HLVTl`t0!r=@6CXzug3UMnm;2 z=hIjYh>(5#pOBrgG>l4mmaZ7>nXtKDc15RgGM*t=^syC6EkeTz0X}@AFt5-Jq^ssH z^?w5VC}=@+t;N9dg|l45wB6}kRGNR#BgjIL-Jy#~Ei#2)I{-gan4z`Z^zNQL>(a7p51Ml`bdr(eDRbtG1r-%OJdD^Ul> z?V}H?07v9ge=LzodQy+0YvcXo9&w{qc&ku#Grj%Md7VGwiD{eeZ%?UmMCzfq6x0mr>iNHJvi^ybh#wCmFV7JrJwI83#Vn?r44=TfNH~)_O8@i>5&YY!EcAUVzsnSHs zIKkfeUVZ^Z7aOtlA4Oo3^tBxjf?NKVfZj1^tQh))xpMIzWEstJxw$G5-MJ$MVb zwv?Ha(wFVVaB~LIcp~LAZvxrQi4<;o)$sn@&0FJvM21>crRWAb>3)d zW$)sk9ZA9Ag(nvii8X1IIn9661VBgZt5A8eEs{#NKZV!>qqAJ8-=D1~4b0drQGUe} z(=OjLxAoWT=A8Rl_U)WwE0MW7kd7}wrjSz(=}eW|2yr%KWd!4(wlXAznp8@%d4A z@7K2@62Jj4|sGV zj6^l+*fn~Q^E}$<5BTLX;@(`NV|Z8v$)|8^SrOvCoY1i-OOa1~#DhBYM1-37-W_qu#JcUYVFVBn*MbKM~HE? z<~*5N^=TMkO=ff&w(Je!G8S0H~mN`g8OI@E1sx3WO6j8*NdZv zex5k`vx~)#Xfat7HpN^6?})c*%cTb9?8Pmo!`MBOo6sc|b?1WnNelnVvi zKoAX^x)`YgA}OQ>xl{2;Tv=Vc3bR0jem!E$aUe0T6FKAy%AS)_;|P-S%t(48ck_}U ziQ(MrZxr}ki&&(K4uxps$1`<{=4LF#YFu{K;>1doI0ZB6|kG-}-s! z)iNwH)?6HU$|eq$UM>bolV0);2Lr262VaLqt|w;?2a_0A$<}5b8Jv*&S)V-^>-LI0 z-Niuz8?`l5UP+G#eOIvw{A>Ieq;LmSH=`FJXlQ|aS627GF(ICV7*Vn$Mw=g8X15`s z@P-SBKSK)c2QTdrvHHA5glSPGl3m~b+QaCtyd|cS;O_2OTFQCSr+|6ZVJ^88C0>cN zPDr|idy2u^1uFB^OSjj0dAq;a!2|2dfyUB2s7*4b`U3dwvlh0{XgvGKrpkZ+9*>O_ zw&h60oLuv=A`#*~zsJ|;N=R~X8b51)7VBgTT~$Em;qHGTK1PACTv#)j^=+QR%z35& zdqffG_FTsVY8+8jkiF2q6;T&szcj)&Iu1?K@G`FTk7IHMMv!=S(27zUuwCltQbUBv z-jx+Ky+DNGq|f9bO{>NGB8V-fcb1E&JM`b^5DxWf+oBKz$R`4-2EJ?%Q_*mY z%kOa}51E5KErw4~e)b?|>lw~yC*Y z*OGN53NLdCu?hYCZn_Vd$%?D2A-dAD&;&kS3|c;Jem_rT-VGrV9U;c3oyfXu$k8|i zC17*221zG~N~1i0z-vcHTGTqZR+7`UdA@?ud%l@vF|e+Q{CC%ki%6H#pN{irI%73~ zA%EvoKb=ow{{hRuH^f^!GNRCfD~mWKlN|O(>Bn87ZrZyh&=+t8SqqR^h8zc4N$CG? z*C5qdukUi7=kJ5b9?_$oUZtf?i8@;dh#j8x^*J9?70KAse8B1%DzDwOyLJL;gGKAO zOyJ1B?O^sVmyJC>gGfoG^sKebVtBlCyy|L88kfn`U+zht`w?L1EOp_Zse4lYEZ0dQdXgt4x)*FBT+5ckew{;px`wFDv=egs z$m0e_M#gNx4PL0-xw?B*ZOh8{mXp^H2rx#keh|Or?SoVleQ(c{e^W&|TLkYoa8ZTn zXAl3HI1cGc9!fb_t<7Hf(lBu&{6Mv@cqieew-4zu2Pxuix(hPbzH-b*8y-*)$c2wP z>cRt(HXk@09qbYdBBF}`V3lsD4a~paSMe4>@Ye@Bzd0bOP-FTTzI;Q?4fu((3BOkF zFcid5OCci6u%U9Nf9gX!`K3hwk*+uXx{AH9cf(v--8m;WzqiLk^#Fp8dM~fk;kxW# zb$%d@!4qudm|si4l}9b-k0E!ipVr=03~n5N8rTmbX3L8kjw3gIVsXd+fTQm@>TiUf z6tg*a_^c{K$5WA!KKf@Id1P+|5JmcATScPo`KvJ5YZGjxyLW$m^~>3s-|KTruBH$u1IgY|IXpZz|t|6y`{4L~#H&eWh`EuSZ9*`$&)`^|pu+3T)w{+?J5V(LdvY*11m|BsSn4C@ z{pkb*L~G-#P=Lvvde%6d6@GpCBwj`?#z!p1ryXrfdpfWfP!`)P2@X#5(eHbCxoaA- zI|}1L?SY8PfM=d>>a=pP<8rvLzx7n6R9(wi$@boo*=>85rvA`A_#q|ze*T#MJaG39 z1>3#Pdu$kNS(|&-u`%1Yz9eSk-y!yQ%-G-m=@+|m0|$l?Tw9pkaF#z@5gds1b$+M` zuwIfyJZlPg2Lr(m{2z{MO}^NG(MPF0NfBWV<^!y}x6t;-6A}p#wHaVAB||GXEob0r z5SEB|aalq9lqRakdcpS3_|N!P*$TpX~j$Q!~FXzFNsiF2l2#Kzec_9!tp-{jltE8~OXdyE>Bvx*G* zMgnrP^oCxsOVnN{)lDV_a5+47gsy6Q46%pTPjfBRvj6Ul)HqEEl9SUl;fN?h9^md5 z{k(bUxuq`vL;}J7j$8)?Yqg~7SL&f_^u@k2mrR{xt{%)(NMY_+>l2(^4yj>BbZ4^| z#{&!PK-1$b0vM-5H^O^GSn{M zK4#(tpKiS`{v<)zn6)cp$m@sJpRP>Ee{*8jz>Si~*Jsv8{tBkuUhH`qVUZNTReer{ z8YWl_sw73i+7|2PJ7n3pSt~D&^0ovWChAIZhQl3D&GiNi9$)KiI!YR|h&jJ+YVUg& zw6v4kS_G&*+?V4%Z1yvAPmja?>;7^xkM6AOam-Wzcpu@QOJf_oYi3$5orZk|snbsS zKXzqZ4pk;FP02r`Z(AqRl}s|kgP|I^9CGh7`p=cExpos~wW&c<#A9h*b#F@^lrF6f ztgB>&b~Lhk$fHvo`^#3;wrZ{zw(eoC`#0MK->~q%;BsPpQ(uuvs*B<4m~#6Qm=JWf z3y48&-=EFjr7~vWvd9=6?f)qAj&&lf{X@(Jb#*VYV#-Jd<=&#!r{Qvnh48hBLAOJN za&Yte8q;Sd?+6Co_pNPk>zLklHgwKbO>0R=XTN>2qqbtR++wfsy~K#vzO!wCw8@9* z*g75z?}q|Aq6Uq8C&=G~N+=fO3TQ7E_de>y;KtYn2So&w9Di*rdt$qVMRDetzX#vJ zS?jSzK~rDWrr7HPhm%(4+{p~dY~10P0+d&ZCv0nUxld1SdgJbBzCh3MRwE;7`j4is zSM*j3Qhc^N`F8p2MdF0?^+ni+J&c0yEo;#HJ-O7m0Z*UlY(+&(L7Mip)q6>ISKTGm z12wKoxODU-q)Y&M9a-3wkLfA^0*t87CPazq13U1jgQkG)Ho_zuVh(3& z5`%W0j+c4ow$-nn^`J&vE2B9*@J;kyqwZd`VOuxcSqNuaT`6@+b$nxbumayqwU7&1 zcpwT4S0nPX1lD8xMc# z5(ae%@97DHhpl4zzO^I6n72DmALvm3`rs$~-ABDOJuL@^`na=bJ zS`}_@9TvZwRkfL)r?2+Tnnxk_a%q=<3Lb#p^Bl6dduWJZ&0eX!pXrgxGidmE+RCkZ zq{GWPGR3BH?%aSsQ3xHY^3Dqo1m4O5%Cl9k3?7R_&KV4U<`C*Bl)=zG{HCes9nQtp zt4Pq(QJYyhJxGE~yCnhLSNA@N0Lf$IPZbm1E@?<~;kcif<5LjT-c zD7cR0m;Jy0?d0iyJfG&CZRZI-aPUp%y=N3M(A`}Y#Ct|*@Djx^BbxMWzKL#H44X## z^)c4b7=o2Rdvb$%Mw~!$Z_tzz68fBnaB_PdbA#~yu=TjDRc!zIAX2*BOvgk~UiR;b z$mtn32_jzq^m7R?xLoPEDg0Lds^65{bCaNMMyEfIT?U@+UNkIA^gz4Pv1$h4eaRN;=xK~1RkNvY#{yGCNN%FG;Og- zB;Ju%kIqKeM(5GF#NBC+kIv?~Yoi|T=FBB~HUfIu|Ajp}`Iq8*5*1a+Y-<`%|SPjSpGEU`+io7y-Y5)p1#SJ8sIc>qUsK2u6(<*oD3QL=6~#J24P=?l@dsJlz;&)9Gc)K(P_fXjU%HNU+J?mS2Ct zjl_IO-k+}SG}z0(0Vy+FmL1M9v{xX1JVcjXN+srPf-d4Wy!CwF&O(!->C zj~4mSuE0H}v%sZ%Nf3%(L@@yRe7EGSc(s z&n@d(>|Y;j<5~7sdr5tQ&|5_CO}VL55$snYtXpBk=ve`)SbW2K4Cdtw4Z^3UgnhK0 zqYB{XbjcFVF_y>rUR0f8>T!q}Knx%lMxam zhTJbEIy-V#m`%p6g}ZY(cZZKR^jw^b`z+t({IgrTesBA*rjFopQU2u7U(`&mZD|9E zUZyu|r)!ft5`KwPK_1_ITMWB;UhJ8V0gG$bU+blefE*VP0|Q%xnx?M49wCvn6x||z zUdT6(&R9*hbiNo4=lQntE!;XWHIXg?;U{cPqz*4MhfyMjLDP2>KqG-0S25^9xhat*lQ69b{9PV`>+ z0obH(V|BbTKK#o)CVN#IMtfl#A+Pv|Ol|@|v(dIcUDSFWmOS^lmhG-eIt2%Sfz`jXVOI z^ds0dy5==Y-#LY;yD+dwe2RU?eaKzOrTQq;tssLIQ6NRO^pP~5Wnu%=Tp*fFH=nsW zprq+#nA5{j3h4Hdav|n11M%lCa9?ndinUR>Y2O=^vMZ&Ktbp3=&YsWH+fPLu%`Z!S z`Rf$NoCp{&?M8qdNgrM<^-C5R5EG!(UN2NRVcK)A(%1T+WVob91-R182{1Mu+z|j4 zbCD31>Zex>FDd(FSe)zUKR#SOTAumvNOI34oVm#oi-X=kG_-&A_tPc3mn$gJ^fhkc1ki8?0vPVVy(cciVNf8M6?Q)p~ z>+)5ra_?alWO+wuH1Co`EfvX?S1Sn1f9qlj-Gq+=$m~W7c)+#piyEd z0>M>8=443V`JED9yqZ|=x8Z8Lm)WMnO?%p&rXCPMuN>*8AzR~1zufyJ#0&6b3rD9X zB2^2y%OG#QBRFE9ewbD}4_;vaTG+9XVqOwVOb!rDb8wU&AQ z)*0@oUBiG^&j9V==xCRuEc++-HE-;ZE*V?We@^K4)J(S!Fi2|Gy5#RPcS%JW_l9mj z;(p2-cs;z<${idtP;!<_bE%xCIg=4%t&g|$2&`0Kwf77jJv+oWqKDIA8_}~ zTk6@?`(Gw#Z1o8RPt(Nz>G>E`+eDw$*g=WuaId4RuZm;0iir2U%3BKSx68TEKIaOt zzHYlpyKP_vB{aY%W2U7SPvcj@V3wzc4Qx+v4)40ayXNl8o@GA|pX(|SfKGl}^vf^C^5H<8tkDLT`F*FKLcMe~tZ zqlnbi9xa8zIl{Sf0h#Z+szWv(2a^hf8kaY3ca{WGA8AtUJSgy_qzar}TWfQV2@tfm z@z@`6j<@5`lp390Be*D7+7UITAXtBPb_mN>w>abi&I6y3r9|I*EQr$g~p_4Rnoif zb^x>H9B9-5?lK`*qtr`z_P_;=USFnR(MG?dhh$su=YE{xW^Bm;T|PGlGa*HIzm-^j z9oI&n!0k6YPf_Jk9dG}cl4|pM)|g0Kcu4MozDm0F0<(bp*9$A#buu(pcPcWVrl8<QK%B`c)elBV5{iCYLds#E#OzV9qy4jyLb zCa#%2ehI-|AW4I}UBJeI)?Ac)A~ok-?Kh$kIzW3h=d{EJ<|N1^znZ+9cZ0bcayarw z;5_5_!0{#yx3iBqv2oUxq_Tr2fM=Hou%^}I7tWsE>ZFJDR7N~M!^UVVzus=fVoK*9 z#=#TvAbbpEX+BauH9fD?bkck{B+EeU@0l$|C*bn^kuNhf{`g=)2QfC;_4b5ik)?oW z{2UzRVn**a`nhL54T7D10pv!~^19b+WBu7>_@N2IO(B_a`=!^-iBIVId}mGIr>1(oiV4*B*LrX5%)LdTrG|MI}^fJ5_h~_iA@v7&g`JK>H|K zJ=B3`j(P4XVo(ggF&9m`)NelmJG*ZLz?{28!xBqKHJNrnUSiKm{20juKt=O7N@V1* zEXds`F2=91g&t3>S9V+cLYuWQ<*AsP)F@YaxxgmBfTI0RFu2rS=8}1gIhQ@W`g%3} zca~A_=dQ@Fas~~Zlmrnt1Cil{Dr0vGAni@oG%}V#4%H(fXd2b)v=R?9ElTIEhaqJY zJ#c;BdKXCtMX`cH^_w{9hUKtChX13lz*X{0NI(&|4lB0#?y#{ai%gO8uIlVxdoe0y zT%DYr9m#3jMee9R;wwF9QV-@2UcW`jc6gIW$=uN#nrF{hdDin2)_v%fpuKc#*~n|z zzAR)wbc#((N|Bsz-do0dP>5a)x=Lm4kppg8jk7f5LQk_v9ubmasY?&vY4q`H*A6bP=}&&|bohbawA z`GN>=+Y1s|V#ib(bcyl+Ex^5(#?!YR9``?@Ei_;aS$;)8|v92*8T7s9$N7z$PGh<(89TYAWL@BSSi zWMlFYLXD1F2%2CY8%3LWlnzg7=hnSnIm&S%kHa=ULd*I_{6XT4a)jVn!1RcApCG87 zF|(5&mAb!mBJo6vN)rcCIYE@CPu70da!K+Jhi(;-iU~(IkGKqzT;HCzFA>^3@NP3o zaLFQAPbuu?Q<@GxCbQd$u=FtXROWQ#=SU{*=G;TuhPLXI z2CLN0pnWn*D7ZP(LHm?cP1zH{>A0R|)n03riPy4S+TClvKyxnpx*=j1%9j&+PeJea z3E=rv8yJcpdq$!8dgz$|E?HhE(*w_VbeP4KVTxueM)}qx*=#_9?#Ztp)9amHQ=x=9 zo85kr{2Kwt=~$7B0m;(I4c?d8WV;-?x4Cz0`rW15-JTGe9R3gjZ2_9uRVulO_Fi zCg?a3lT)6XqSjh3n;(0_gOa&g*T-#jX9>l}eh2Pq5HezMCKOpkR0OiSg7 zyuHwb(N8vZpONs~2=|z9wN@K0dnIO(lK4gMZFbf5@GrZpvHTi`PU7PKhxxHrpoYcR zS`+?vn~`}m-j_1yTLcWbu`4noB_p2Z+Wlf^Lp9H%P@eSIn-PFR3nWSsvLFuWRJmwi z+yePDx-f?ZW_4Ow;Q6|mcI@)uy7H1kaUnV3JcOj|9Y@8B>_?F~y{V)e>Co8-6yb2c z=h@XiLFOKyz{;ZLzLXlzYnZaZGi6iPw8hzEO-D6ZpDJ3yW|LqwTs^)p@EwH(WO35~ zS#xL=F1naL`sm%h*eD?AQBXnREjpP&?uYl|l-ZdhoSmZY^Y&gM<+c@8izdawOLPu) znZ@J#mPp5qdydi;d*8TEm4MrAAY<`UP7e()qMNi!YQ(5>Tt=n&2g|A4M8LY>70K1b zckB#2lR^w+E{Op7HNdZ80I@GlV+P333KyY{QzHNi*k1DkNV>rpo1tvIlNdke4EVMD zv;vLN;NYwXOv5c=t*J)vjMjXB9JQaCoh*bG6m8?5NTu{eS&||~S6}oyNA+LBsp$3d zy6?hzil6I~30Hc7uU@w_>H0gIq_8H&UJ$9QwWu+g{;`v0HC1){z*nw)IpVRCl3EmPPhrnt*( zVghUx#f?2Ga5F0CHPczg4=~$+T@i?b^0*7>GB{9oPUiP%_IcR>sT-hIM z!6e))!LU=NAf6?nQ@$miBXuEXdy^JCxzuMnZ%oC_%)D!4Mh~@T#K$ETlF7UF-F1d_ zq}+hK@#_^y{Dq)Lv12BC?=J>qT4g>=(H??oXTxL zjEfIhFt|k{IB~wHg)Zt$lWxa^c2Q2Zt2j>qYBrjW2CR$^WGq-qtJ7ke?>XtvJ-^GJ z`|+i+28Xrmmc@t9A{OU+5V4hBhf8<#TZIn3#ozSw`|FqeX?%ZKoS@vYOzMe?ZSMny z^x%;cj_k{n@~9{WnFv`Q*vJpJqNmIsKwy=y=JX-i3>C(rjh-ri*bC5fXN-#=`n1uA z&oWq@qTCMBOP#xIhc&I7%@Ro_J4|$MO*AQ!8{X3P@Lxuo2Iao&Q?cky;Q@FMSP;DM z1M_4}1Z7^wf`{{rTT-w(5%?r(X41kb7QEdDpi}QN$?!v!Z^DEpg_#l<`+-^6usERQ zDNTVs%awBu&b&B;P3ucd{#jvM2YDc;cU2>VNV^ZFFmB!rxJ<2vH-8hG2B02lK14S` zZTQoDmpM8<0XREk=|FM5nm-7s4E-na9`L z1O~~bhIr)l$sdI+^Jx%3y@MUVS)nN-oFru7mL^?k^nexcK!uJ8Ic_RB$jxYzUvv@2 zj+5YK2mPOxD}UOrD>?oaC77_2D)yd2@mAQmz_iJY58(dGot#?3a+gtCMLa4!Dc^r_pDR8DDJ*Fb}q2AT{xTXQ0ZzIYA=c^IRJ?{y{X@|-Kx9#*Ix8* zMkcTs?>G%#-?&0Hr8M&N24+Yh6mr)1_=)@GSxBU<=OH-)z+FSTM~Cb2)D_7+w!{g) zg~KC#m0X6;h#dF!rPLq%=<%I3rxGC*{x&ep!}nXPhR>c(_4rx|?ph1HfTc*S9iCCk z*)wdYPF#KuYDY=}QCH}=`Ji8eqR=#|%QTdVo|VaXE0&OVTk?M8%Eeb4@64BZvn`nN z+MJ>pZQ4ifpzMZz+ zLK~<;VR;HQR1t=cnK}?M(6-E>&+Pdtrza)yiXuGal75f8H~WZ`P0IV+kFSo8J|W{2 zZ%p6?jz7mYJ-+#WO&w}M1Xp|XPVzXG`~1R@0Ws!ct$GD4_#QbohCwJ=GOQ5t)xu-9 zgOVE$Rp>Mo`TI+~qR^%5iZ}T^ze>)enoheQvj~6rYOSb-G3A@?a$)R|drsn*8FCFr z1L62v{B(KpQ(p3Op9^Eoa|WYt<8UP-Po${3%`4 zNPNcu(?d>uFQ={^y@6LCoGbL?U`*aTrbwbyF9b~8H!1%TxNQpqCul)xPN-g>xssm^ zw#=bN3|JZJV+K62ZU=2OLLyjCO@wXb_pCh?k;izssxRE*0Wo-q7rm}R8X-%hk?TQ! z^3JdL0QFd$P2*^B?0t%ydu3;7T-4|ft#0PZdSiUTl;=3AeIE3*XU3jmf~?J<9jzzX zCMfdN8#cz@qYE{_!Ubf6%Zl#RZbY`rm7z|ARJXtFhhAioUT1Y`qyOgP;gIR3&;$C8 zB|f@eZUAWu@rxSCKyF3cr;;PZ=a1Q+io{3M<3_ zcJwO6i}EECGIVB|iZF!#=6?Nl@vBgbro?mS2SafY7UsF)u=C8#b#nefnIhzY%GU+3 zTGD)wEoqA+pUV7*I=1a)9e`E^e5ivT@?y2J$Q1T*yj+#4$m47am}a%Hwdts0SRrt| zaU+ca!zxT7!0i?YBR4A(eD(FR_`frA&o8zex(;>TcsjpSgw*UH_a zkmM|bIa@N24hYjg?gg(e>%H&@ZM$!?mEFO0*hsDQBuye{R#%{LG?ZAro8BbBCgK{O zF?elZSku9h)$%J5tHA}{0YH4EpG~g`nKeNS@fbwKR_*LexuEM!MUobO%>@G0G4fz>v+v%q>A2MViMk z=j=?Y&-{J`n~C>S7N(M(r!Y|8yY{F>KlR4@2+F3x$7IJndMb3NP+^@iW@z3Krqfh} zrR!9RBKD;q3zCqQIkYorLaahtQ&G5-o%Ot|=I-4@UWKH#$nwm&{|;7;-~o*j1X4e& z;dk5pyWdW4XyqdYv^fdu&`(>t-J>E>5oEYyGc>KK7>i7CsxpLu;u7T8>s7!5eRlSB z=~)&mk`xh=9crzOyWFpfyDr1Rd|As5p44`0NRF*6EoyZA#oOwx{?!-#Kk`HW>#cD| zEx6t!4ds;wXlzTIr^KbcKM(#W%$eCg$m5`}bfzLWPgPZCf2X!vyo=?4TZ`KrVqX!N z8|XiG1`_E7Y1c+~eGQeu0vyQ{a#dPlu_Oys5!oAe>{b<7!pl8QVJ}x}r4Ygw2_ehN z`l;ltad_IPDzmbL0du}IlCcXpdi(a><=m=xxr(aJewTPV@z`fsumu;Hx+E3zyD3)M zyNgXd^Wcg-GxkFSu-&^aUYy1(bCW}O=vK2@ehnZPqzu#Sc>v+tWS>RI8L);t`xR6B zW$t34iTFJqM+FT07yaIn5v(t*GpO%a!*=`RQb*(||9K)wMysR1{`Aq2IkHO{b^p+yBN(wt= z7_XJB0xEz)-Ewj6-4iwph^6!cfiiXg=ZUog(!Ub#q0ORz^4*{|yXUHObn+&+^Ek%y zVxG`>K(isQc|NO1B6#Xa+FIkBqIA&KG&0=vv?H{?0yzR~Q3pQ;mnn?$mTEw>c_l6l z*^oQYrjc)skRa!<2xZAJMeo9bB3&3Lv$&`7>}7O4l;N@pKYK2m;X-~!iN#(P*OVFS znT<#aBzkxD*A7cxZ`v#Gh($G~#Qni)6Cz5Eu)ePzy7st$#S$;)?kM70Yq+CTX*t|Q zUY|$p!%nv}v5cF$^U4lOoV5Tr=N0sqRA)3R`j45bYTtq*) zv7?V4Y!G+lDS1RGY6W~7m{(s?60|X7RP-h3_Y5>6E~0<6ErHe8lEJO>H3po+YW#Yq zb~H+(x!?tfY=NZZT|z)`R%mqrsB$kPb5{?d-YS~pyeh$BjEQ_-VIZD$=!7Q!tPy|G zw>Y(on(DF%j)k4Bx(2+tz><)SA!6X2B44o=zFhY3{9T-c(P$)Z@mTJGtpkw~j2flH z5nq|H{!B80bS&Cv^9KlJ;e0kX7cNfZXZvjPjXW-!S>E}7Ma$%dROtZhonZ@cSWx)Z zk!jQKu!z>3$CRQQ7`g!F>@+rziAFi)>^l;zdSWm3;O8s?Vp1MSdOg36c6IU1o}0WU zef#Di<%cE&=$zE~OEtJ?T2%nb!Gsz=^cT=fx1N9Xr zgaNO%@c`ag>=Y5+Q+Za=I2;l+s1?-$5mSL__PCuW8n6aLhfB1EinY26+AGB-`DGz3 zb)8UzWqs?DgFw|SDy9q=N|E~(X^)#b^liUub?4s&W&5zUQ2R;-hWv^H1+vt zzN^lZP!zj*0mSHePZlhttXtv$ju0B5?h2U0fakye>75swN1&UB&<9IKuy2W*>ieWY zn1)B#X+$f3bAkHFDl=!xQldsz+Y@#(eHrr)HH(UTrz%ifU@L%jzeXL=WRh5A1c41f zsPelSKZ(5&XCN#}(+eGR>Y-L$6p2y_n-3e;1WJ*So(OSKe# zXI?;-K2l^c#)KV-CPdsIyy|oF>GbelJeXxI2^hl=T>i6Sp)Qo8>JA9djmp>*wSlvg zDcVQN$tTN~8D}H2PFKzJaQ|K>mDxMITM(r+q{#i;^Pd%|4q2unhSQ)btS=TX@CG;W~ZCIV36g({zr zWn-2o`?ZFdYo1>_vH2ihZsc4aYc-QMJ0s8(ICrhr_jv!LAj(#T5OFG|d2eAr=7w*L z1fmga+;M{?hxA0mcihp*(e5iQZ7O#e1qhcOh%o>aFcCl|aJ+=Oh>lx`J=cdxCOm`} z$^=a1>aIQ4Pd?DeWYl4ikspm3shY2`L9GVa?)IyDP5&w4Fe$PvIGV?7={VZy(7kQu zacKIGjQU(eHap><1;!`XDXJ2!ao?=R5R@|1VnVo~OvVMYM}}Rg7+uky94~1SuGU{; z*GEpxyK3wh??p-nj$zW21{e8RsQH)M29EW7aeE?CMP)KOjV?C1zL`(;kDCT4q{&-b zBGgIxIeN|$%dZ+mYXk9Q#WLhKhtcoZDI+*(uxBg#Uq5>rHR>8K>l&Y7A|2=A5tl)F z7@MrT`=iIV`IOi*<{dPRic`{0rPAhuC0?$IDnZ%L?e zmJB;tfN7L|&!dU~-t4bHLdeF@>g!CWU$@GAneY;=dU@lRNOy$^pwwkl@SwVvdjBa* z5Ca&=dSjBZ6Vz;VQbs&i7leK#9dZ{MV_aM!M3oJlRZQv1xqC8s6#`+br8714fxq0U zZ9{j&ZT|6FJM~Nq2#Ax3M;&@8GuJxx1QlH4Gp3*!IB3Sx$YO}3yp`8;aaOUBp~nvC zb{{RNu8o1l3t;>aedsK4FkT7sk9aK37Zngg zFvKa?SkXLJF*)@~Qm<(XxrQ0o#k_=uJ|C&aB)+k!&nkGUh zk`}H?wIG8`Zy~xadUDlwoOA8NY|$PTtUXgz;J+)I$j$gH)_~56`}dDu}+@{9^5BrJHxN1 za6Vjq1qM1+s-^ht5Q28^+sZA}m_P*MuBw$q;^iuKD3ZKH(l3{)JmkoEv60Llm56p~ zDatIPac$3JTpD95pumn4j_crp`ew183e6-N_cm6Fo{} z-pXtOza7JrIjN&&sw)vyV>`>LpW5JEY3pWUNop(P(5f?ugx9g~4Y+k+n?8H3)8Ztl zof;a%1WfuX&u+#6kuH}spWkI;G{y`V7bF4(idTSW&2Gei9}!~Q@T-O z?$n1I=-vkYDj+pG6*RGzyYG(YV?dvn-4ixrO9}Ez2q5s1D!`q`4TiS7wxkh7(*yEA zWEVjU9#D`N_i9Bq9HuPfI17lOucW&~Q*`Fk_1ztzfSxWviwA0_yR^Wid6dCyRGMlAEJnMfw-lw&J6j@ARS51+t(081wMD6B@r7BP0@1*Kdpg-{Q zJjkk_tnQbsG3pd=Cr+_#jIQe9FSw*QRb726piefZ^ca;j$~sMx0a4xv*c982tO<1) zu(HRzXMrR~U+KmqHrT303 z1EN#(m?%5)DQ?vW7qO(L-_80oFsLMjDxg2?3`K6~yAIQYbZ7BGl_y0IqFq~AiF>$v z^b$#{npW>r9`g2H()HGTAbG8xJoIu$tnYCH3`7YAlda#dQWsnj@du5uDBsaSUzi;H|8XH#K9&4t1~i< zCbOdGv7?ZUy=!y^@)L(M(JgOib3|=skMgs>Jpj&yG3f}7b#A?m<(EbsFx&2Z3re~= zLiL3_d`d*^jn_;g%K03IS!MgQx>X#V2$O4la05w_(9J`_mxG+U3t9J6zTeW+!R=_= zC_km}+F@}`us1$>wT@*o1ajT9TwY5`fy4wLSaZ0ft+7ZE_vACTE z@@=J7Jwb&ohwebbom3as5jbk0O(Ttx9rjG<0Z*({{uEGbr@-%-4P{UnjhaI*JWqrU zee-APKa(*CEyJC$TR#@k@Y>!lDg1Ss6@!K+wgMjxh+=&j(mm!-*u^h> z9O`1(XK)fPL3Y+CrAI@9se}N^$nSB68>LG%GWb96RGu|Z7}QeqcHqxVj9dC9>|%Ji zk!3o*$#d_Y0J-&>Tf5RIBFW|y(Y-4$px1Jr@3G|+_fDXX({yYSo3W)!m7{77shmR6 zbo5~~>6AqdJ%XD?XVPB^%W_$P-!u7Jp#r8MSJwa&VKL^V!kx?M=<=)na4Vy-$Uj4A z1;o1A9L)z15~)`r&^!-{rKejcT$nIShB#Buf&xgpWYFXwfwMmlae-|Zh70@UO77hP}eJU*Bk zsXxhp^;{qG)z;C`@qY0N1U*kCaf1OKf3c1K&|0bJ?C;fb+c%kT89x6#>02?AXZGsk zxMm!smk8JXV8>E~wqE+thKI8UdJu~!fHlT~X3tn=5b=40TeV>UmJswBRMy z7eeZ?{hcY@6m;WPB1K_SIidKfl~H)_Y0Hx8qb>_ar)q5{M-g|f{7)_uU1Za>c@(Bi zW9n`&@7*XlI_&ExTAOAkdDcZ;uh)>harZ5+$_>B07(JrkGUx)5>Q;I3S{$L#j zudV;daqiqXfz5gFiJq02YTWMi+KlnFlC|Rz3AS2XY;8v5?vbDmJ$6mg=Y5;Tl(Q8? zGwM3UP7`^grIO4lV?}RZuONm{=9g){$-+p7{wcAiz}2&I^iqcaJYg`B4;lG|^H(7V z9#@&(^!8|ZCP7)S42VdOO9c9A`KZgqLRjU5DV{4oC9It_rm9qM@5G~wni$nQrxO?v z3R$37O=dk0NsHP*uUXlxz2T>|QPoLV^x~k;#F@)NDs3wom*hM2)y7sCsCb?kZhO*1 zY>g1_DBlbe)T*ljxX*hzs8Wp`Q{ zmf{7u>wE7m#r*~9&@4eV zWww|E#2HUo z2z64}4Ljb535cnp3I1Gj>V#^7?iFssJnh)S%w7OSt^D85-tInRxu5n5IW(X&Q z;I#L91N+`nhDyq2B2Crj*}_Y`#%YQ>;cUR+fyAq z_bf4b-IRt7xDO!LOX#CQxS`4htme@q<77XY<^NpTWU+6aOda^7h2$S=Q%3bvjlstl zC?zcgB*-&?Yd_l;Qi6;4?8Vs1-ZS>p;)X!D#@gV+<0nLYMlNO+m9^7cl)}D_q(zSA z%%La1Tph_BZpMaOl0>UJmp#0PE*ifpTG2>+w;*=2W=!xD)29;W!DT&LtB}GJ>n<&~ zC%06maszA~Y;t|q96>6X8Gx`rDx|i&jNhA;*eURDUAx@^Irc%MzbDsUDAiK**5>zK zXHPeZ)Z?;OWhyMno>ppg@K^_Wfi^Y~1 z<@#Kx;w*3kN6bzJ?OM2iq*v?+H!00(scny;^PVg?D>RZ+Df?~?tv&F7c@T{b&Eq=K z!yvfs5jwxsWUgB8nD*JKd#MoAx1N>+Z_%HLqNS)@4;v3Oe4hP!2d}c&J?Wq*!gqS^ zR~9Nv(Uoq}iRZ#Ue4$n`qwMLhWrE$LI`6m)Ss5R!&)j_1shZ%d;bQ(uH^<_lXrlsi zh)-MczI?e$_T~QAqRQe>?frvUR@>iSqCO73pc+30Uc3UU(`xGXYJ3}qU;6|hYelXJ0G!1)xG5^zJxV3;H>EQqYx)=hB3=7RiNH@d@*h;|O z0~9C7QCbk!_za<;f+S~!5_k@+sAwc&=CJ0y7#V-x_V?^eJy>i`^+07dTqUDcH{y!= z+KlUtD#7 zIi&)tu9HIp*rvFn2}$EFGVN*q{Hg`8NKcQ}oJaWG$BN!=fyn+~*X$`Y#@I+Gzl^|m z+GG}lLS_9w5E{Q&B>xB+Tel@`h0CXY3ZOV5rYwjk$Av-5lwGw}%FI>7Nal1&c~oSQ zUu3U8i;a;_UxXHeE zU(C{!{FJ)9de)uT7kDN{yORz!(Ty#kiCf;vEZ3_voi1wAIcPC`0%957B5%jO!)hY1 z2qvBv7U2*Gbl)qa}oDJ%wC28=;k9aE13fSn&cp?n!xXt9o-nNR{_>5g)Wjo$uqOg5PH0u6 zUG&=QmlYD86$Cb{9(I$z*Z%eA*!xm6*lt+*i=YfLm!Jga3NZT&_(}tWyX?ft5Nd=G ziO%;BO023K4?OqN<=a8&ef_Fwe*JABQtO?xP_>7~^afX`Aul$Gf*Sk7n`2Ek)W-N! zmAd&~7yOILdRLkA*E*Dc2LfzBy8j{H8BW1AJVMxGLDCH>m?2Q$vS%L*!MJ%y zBe4`-GzmkTGZIyOW~`Si{?DDkcRpj^YX`p9%oQvIgU3zVKX)z1T31bssfQA;INW2Y z8$2IME}D7|61E;Id3k@d@SP|ot*4iN;h2ZOe0P8HG&hy zc%#$!a^HbXyTh9UCH(vz%WvBMTEAW6wJ>Q2e+N1w#2rnTY?l~7G&2k&Fo3~1QoZQ0 z`&}U66YWi#m%z@ktD`8|;q%L^pR$i`xp8<8b_$nXd0E7VK8{cgQnKya{vmYy=1JPP z%r)B63-nj`4oXi>tCQlw-%q_dVY@R7?HGQ2Z~mR*O?osfFe=A1!PmY4)450Hyarsk z@G3|Y=}ddY zu6`)()}(m!o46BkW5D3bg-R_Tlz1PCya8fFvGzXbfJ3aSOaXg;9g}9a+OdW)616{V zME@@3JxWJ29I=;^eMsB%jPzQAoFWvuq&l-_mTCGZgOayxju%1HaxZzsb~9}Q`Q~<_ zS5|jVCyBL4T~O7}>^wm%`WPr(Nbs{;TK;d*miV{kLKx?(zs~`iiL^+WF4#nPqU@{y zFdC7V(ak>W24w;eiOz0Q-W!t_{XRVT_@&Gzt9xKeekO2X7xmTa?fRdzeKz(ToS7OI z0x_h8hTz*L9UOPe-KYHLS0wzrjn5Nkv8fKz@a+=+3mNBtv$~Au(zof&6d0}EFqrMp z|BYZ>LIc31@-6|+nyjnO60a-(}>Wb2O=`dnE-iPbO0y1hG};8mKU}fg5@*| zzafm3(8bCcZCZtuJCt3jqEF0oltKj%a)U#XW6481LIHagnLLm`mne5Qqe1#PJ>b51 zh0l^w{$g+1cOIcwJinJS_2_PEJlZbtA4yAAJ3-#s|ySmXBmq4;rPLf=PNeFEzaSbE4=W*j7X; zx1ClU|53>Ix=@u*Rf*p+=(>o?RVVnG2f|Y+CGOa0yTt!L>Vrgy0t>Jvg?)O2Q!)G2 z#YuU_f zqX7GbR~Ria*u2-g62Je(Pk#Bf!=0>ml}{YjBaP+_?|OHaKSHUVnb&jiMBU@vJ1x(` z+jM3R&Y1nCOUR*9W01E6XvL%3MUa<)s_t@Jh!O`qz<)vKBP0RBEu%}C zX)ab{r#H-yNAt8W4*vot25HE|O}o8VUpBvgmTxEeWKGsQLpp!Bp`tF15B*W0A1uAB z;pI0){(o7LFo29a<759)Ev)@LZ03#G^zq2M&oc6iYR`{*Aj`KETooDVfYNRSA7ed!PKV_$m#;2j_{Jq(;M8;BBKKKoqn@bu~m1J-{_L@ayPCa#Y*g4mE|{HM7e(}T(r%gAgfs& zlctO%QONWKswq(RsmXKfx$a4)oH#`3c07l?=Z}rcZ(hCgIs9HrG+&K;GMxHak~Jz@ zfV?$gOB$BO3%>q+?HaW?Dl;yk4lonPesar^4$F~=J}r~A10S5shmv;dxf4fC3MGDz3^A2(0Am(Zd#`qb?w4Vg4!LJ$O#P)L%vV zpx^!se|T~QbWM-}Dzp0FZ{nxU4-SFZ^g7gpsENyvltlwfwIzNyrn~$y-#$Tc;ApypP;_E-~I@uYV3SHh9(rbb5Y{#G*73ZK% zWYL5c&zEI(OLLJ674B0#k**p}nze#u1&Z+q|@WP}(hYl?qRrCeJ z<}D6(|MJeif11r3>?TeA?tY70konSpq09<@p5MfH)6TTVk#Q@b>!7IZb7+Z|#lL+O z;Uba?;AST#r+V0E{B%m)oepytI~UXcG;pd`VU&J`xiW1wo{Hcs1@E0>>o<)nAYnT+ z{bmSQ^aPAS&0GS!NYF`tTWu>8Ii+fm<>?cKag=rKo$bjJR!+^X4aaWvb#|fQvLRcv z;rLOGTR+)ME&xCawWNk$*!a9b(I{Fk%c3Oz43;F2{FrYio?tbH+om;pxwPnHbEWk({^lED z^uOb1v_!OHFK}q{4m~n*JeKb*U|mk`A{`!=Dgw4|CN$)5Z{EPFH2jlsFm*HFa>5=T zL?Jg8KNt=Qe1N5>B9)27L!4H@A_t_Bak}j`^v%Qj~E#*W~eirhzbwB5wbHdGZy2b94O0X6)IP=DO$VfzwoD> zev7W-3fSG&5Oem4cQ^|b%MD`zy)Y?|p#$2E`R;KZKTQLk5>fQPhcBXhoRJphP=q%b z$eEr*1MXlTEhk^zV2_!j%q!tEyHTh`exWFF6)7sG)x_i9TMgy^DG@Cwjwt`+)hLtK zmZu?Gl8lnbfqac;ZvXUI3dI~cFNmSj<9mw*jKg6dDU%>_`CNhJXh8($@84*zfk_%u z1iR}&{@6krj=a426mz(<9PQnEx~z=YNF={vAahieEg`3$e9rc_La-#WPO@D8P(Bt- zAWn6Rt`5%5{O*0u>X_YyTzky3_87Tl)HT3v#J17zcSiHoQ}3N8kusJNp!`Nl_y>S1&;Db z#LFR~+9VD7kcNA|N1fQY0qMrfA*`m>UmJTM72P8}Rar>=bQZsyqpfzT!hs(M^74F5 z`QpNqEP!^2%cZqFgP%Xus`$tbJZH6ZLWT4c(Ct+?s=oHY|Efa8mXP?{e=xD3=4!W) zXr{wVdj>slqHdr$%4y5XVX(nbrClAD9{pjOr}!-Sd7sW$lFnGt<1%*7<$z8%QJ%zs=n(z(MaezVyYY&1?{0BQg8lU7;^Nt>WuQ(m3KNpK_YKkw zzpn$p*#c|O{Sc1=)VA4R_@8i!H%c!q4sMDtj+nIgV5ip0)61~iF|TZRym5ZKX})uK z2vj89QG9dU5LORD2x$44N3q_t;L_`9gJ1YC-FQH9k2Kd3^n!GuZJlacVS0}gt*V6% z2>|T9_tZ(Ek8)1|DIt1cc9{nUN2sQtu$4P?#yrRq3i`mTB!Bgsb&_-jXi}&`cb~KdE-s@2yl?#c z?}5t##90OQEYvCXFwp6@l?cKR&DCC@k>H$?9x(RiAYMG(3CHuBLq45>q2V?biS zlJ_86Rg9Bg`%F0KnF(y7^MPp=jXy()>n3JtM>g+ldgJ%PPt&s*wZrFwsY`4UZ^=c7B5godicQ&*qByP2&x%R6qKrZk??l8uFi_OCt ztJGL(-RM_OwGbWTN!?Wxyz4`Qw-mfXopcV1#Za^zVPu-k`x?!Vx$w3WFb<$0cVHCi zm|K34ky~fv<&bRVaey2A0wjq>L@brb-^X(lg%C%H-|(#)3HS9bgoXGu9LiCl4uv~- zzfn=TGrjF~0UWEH!#;W+o%8D1JF*=o#p7u~h)gAU6=8k8e+&@IeH(&KSeGb=e8hn< zTIf$P^Uec|`Bwx;qFo>*n9adTqtUe$N00#&3@=VFfK_YT0Y4;HK_FR zSQ$9Cpz(eaF@pIp8b2MKM!#m#;Xu7G@=Xhok*a?R5k6cBMTme9;$TrKka;{{iAXwk zz6>S43$K}(<@I3nnwsE1qCc^F2E)HN820N)QByYWBdLoKR#;ggetO_U9AOQr^DFV$ zbg`i$zT;%c(tMTu2N6lwvRFdB{!inZ>h{_no^>!{m zKxyN*_4HixHj*CPIct?@Q$CB|3fXL=wV5z)qrce60U;a;nVEx{KbW#+C=so#y^Uhf zS&VX7Vje);Ne|9BR;@?z5h=fEppD)zCg|uubMr}(ACxx@_Jf%0x_EL!$lv{7^QI$B z>whlNHMab!jJ+^>dBV!oc<`Nyig@3s+BWMiyOU3)webJ!%SsAimTl#5@blr=^Z3$p zM>d*BFphuBw?`qv!8zv{)$rLh@qbWItxbpUyre*eX2`W59S5UB#AhBH+%7ba0=@re zNNq#kOko=0Bm_LUgJK`!Gb$A(WG{0w)1qa z9D)f4W3gzVcin75(hMN))niYb8VQ=ABpv9MR&qjsu|;j4@ndU?1Qj*Hj`NI)b$IPkkDbmRTT^sqv`Z0~c{r01+o1J9iX*p1jXvM@^il|N=dF5_)*AMp!% zEfs@c!aH9lf}Z9KlV^7l;Pjn=ipA_m9U(&5ZM3PB%plqR1kG(A_UFZ0w=@>%>{Dsv&Ut> zORN7&G{-c~ww4%?PUX)i@Ad&7#@%O$&F1%aW< zKrCDf=#q$V6$fKV!GHBhZves6POSjNyKW`~U3PA{F5eCHzIA;kH^u7wPOUtXozcox z+(?q?VKE1x+idIl>;D3ycL@ZqWrm|s`I#^`{eqQ&{yRi=N79`ksUoR%kkq5!L4rBTO=6s!LHEVqW!ER!M!<%OS1 zlCSAGHV*%#qw32y{DS}Ds6G-PPW%X;&yt^gM(>#S_6|`jiK-;+eYYBT-JN-wzw38B z?MOz($jp1;^W|Pds|6YtX`j)2;mx@km?RVE!<|7`tuS4?6vKHdKn*fVO^6!Lc39W8 zm(w?ThYYk+y1ILGc49DEgcIYyCm-FP7&7^1%pY+Rm2jC6*q`$C<(eT*t_A&=Gi@`S z4qxpvS~^5CZv+)kXBPb=CEwxri<@ORv8Q=f4|P%>cI$sCLSgVC_w@5?4~YeSaYAfJ z&`B0?Kqm&$F7rKdV)_f`LT0y-pb$r?LoIJzCxsM%u$ntUEiZe8oiwWwHi$D*vvtM) z3`c_J5=vg;&nvqSKkVU_OF;&!<3N(M&MPw=foxp6lov_+^R$3^(D9spI`!=5Jfl{h z8#Q*sjW&z_y-WiqH}C~>ijty7;GPK6A%>X{j+{>XWaM*{P6)75bS;vb6Hd9!Gy7<< zZ1xm`S0yMFJ{n|T`KQgb*l@G?m-k02rzi6iSd5?dy-7zcP7hCX4*T5vXb1C9#44GU zGfN+!@Nsj7n`fOYzT@nrDlygbzacd2{C5O&jT|ikNWPY{6WR;Jm`gVoyq3HKd%I}mWX&Sn z_7P>?a${g1;v`h{klo9`%;H}m14|`FyWUg_SR6Zy5zJ3Rop%#Vnml0Uk;L__8(1ak zqpS;!VFSE&tb-S)Qi47_YD@vb9MYea};F=%Np(mspg-jI#WmRhBShGFD z?oAH6x;}YTrlKU=o54Z&+t+qBcw{X7Uo;|{K^(MeYrM|aE1tfN?+f@A3qKM=Q1y+pwBZ> z=Z0A(gcUTH6(bD66kQjAf!_E7PT<@uO4&=^2uYzhF`2z-j=87Sp_Oe?1GUDeUr&l= zWLbc+b7tfteve%~U%U!eFFzx0uz)rBz0z|Sdrl`lIb=#|c7Xl&gSqJV($V$UU_%A12Fy$sX zmz55F4)b$!|E=IB`%HnwIKCz-*gf7j9tT4Q(K&k(y(@prUjDIdT--2p!A8SRp(Z#m zrXR#)2EKw~1C%ex*rBs>NyIy(GWY_KFDj5PDoX7UlAp74D;(=kho7cq8>D&*aT*+< zCl!$EJQqnwPCkLwps(n2gr7*Qcs|Ur(XX_k^c^Ql4(8QAaS`l{cKXh&s@&Z6gTvKJ z_#%IyWCUSbfjwcMuN7EMhk7&Kak$hV3S8D4lWvIW=8x+F5QV&EeRD3rzHwlR=jS}b z-NA^3#+7Tv44-q)gQtefC6xKSv+09;C5APAh7!^IEF5cA=VChUGH^+3?+Vc3==7Et z8nS9s6iS%fN2b`7qK-MvuKaI+;66n#^(@6Xhno=Z>)e0^cRO&$;gW(pgcm5}W5;k` zp#iYTlu=f(Ks3TLakpa&-)nFv(27TL5Qlym8m8PnZ@|j9i5(-mD<5Vj*{$W}PtIuo zTXyDh1>2^9sedx|yZ8B2vpr@)&vh?jq{;HEmUwPzeJEaJ*u=uCDKBu}^V0uZ?KGSB zax2h@d8(G3SFU;Z?jvrf$=%_1u>0@hk5wH+oSVESVXZ^vI{CTx`&Pz9EFIZ(wrg#a zsq<8R-+O9Uz`Bw=;9F7}72L+zlwlRL)=*WS{M0vS;m`idkBa9q>pVXwHi5(eeA8Z+ z8U(>?cfen4Aa?_@02_i(oCkJVw$?LUF(>IlsGTi|)ZfRkdTmhuoYy^1OqhxYN@s+F z{Lscy_rHQ~*@(3i=Puf(^GR|@Gv^~WmknzTx1a~u3l{js7gK&-bY>+0S&A=6(QKO` zB%*jQhS*BsYlki$v}#b11TJd^!p(UHmL=UFica`?&#f%^#}mzGe}ck}D`-mp@lHeV z63XVr+0L5$(f8~whLswj;ReouSDHKqVfug$bqfg)RGz%GlzjNVWRFryfDUlNqQegU zS_t-~$9GHZO|!Ox(MTt%^Eg-JXRYfdRh9H%zr~IAPSbd_F10y*j+TA7R_~~tifVqu zu82fR>$b{ZIDws&}ckbqC)8Aj+@!}`z$}!KK}?VVGGOb?woU8jsxlR$wFpmuGBVx7)hMjUyaPrEW-4QaGYG7LZ< zB=hLA_-ae_V~$H_!w!~>BbK5$P6uc410s)>&po5h{T`GjJ8E+|KQym244!FR!0MR% zxY5s`4RC(AV3;&(D9kH*0tDlbip>;93f;r!p9Z!>xI zr1EZY4*H4rjaa#@oB?}3(2dZlYBD+i1hA3hbjorGCv{MepppFF*UL3WjU$#%>d%gY zRPog)BD=Bs9~aAW0y8mH~{T&|~Yy}vK(03A!V@Hdy z$rw{-@d+I2e;t!*CT>`Jwd~KToz6HDr)WQK>Cs$N*xzpSpyuPwC->BJwMNLSP$`G8 z)9R+IxRVG`C$WJFm?G;R_{6KY^aXMEl3clE<^6#C{QLkBqA2ee{ntbgekVL9R3Y2= zg~>%faM}0AyW=P6OUq1Yk&m=K(}G@XYBlPP;q4ND9GR>cc!u1ID$Nn&ZI9&K6)%9>aK#gxtIK!h+Ru59;%(PTjmV zTh9Kv?6wjQriwy{?XRiNTZM#Asi@=iKKYc#V@CX%8Ple6d;{g0rBT7%z!RW~DsHMm zN{QbvrPA~MRb%q#U3SY4b6tqk#5lu#LbMweJOTxAgwWE_+n18V?{_vxJ_()ENTU4L z2|R+R#)&>^<>6Wnm_M?Oi_nF%?^e^N0I*xT;rQ2yATB+6qx4RGy>vc3u$F1nr@&2` z_AJWn;7=)_&a3qLP7;5Bb~+`JTL3^tCnl3c?U09KA&~+IZ%ao%6Ib2j%hho4DXRD- zb&mJvNAxZ`PC{fCg>{$X>fP(z_F8Z;m!uWjObJs1^dAmd8Nn-{M6Jm>wOyii$kHahRVtTrURm8JB5$Edl#*noZf9N`k?r48eo`V>CE6Dv3((P-4+Gs8l41p) zO4E&DPy?YZXpbnf*KN8Aq@uroB>Nvi7v8&r+g3y*+IDd(YA3@h*V-QLds>_y2jOmF z%IC1XKbV1TVyCGpU$3uDDaCLeQlSQzigTI*&-5ftwAECH)cGvy_e9g^CrVwFqKa!w zsWR(Ry~7S_kMz4155V%VbNQu*{FJZTUz}V2m^Kc03VXd5_v-CeHg?S>>H|4;t$VXM z8i#NQka(FMKEioL3yp7eOS}oR_xBshb=}I~y~=`Y5H+Y8yZ!CkZ+JyJ2y;iKNfR8` zx{@2R(9NPwGB?D@|EHT7=m54WX2X`dUKQepYTHg#wi-%;s?JN56K-NQbHw^`{&(jw zC$ojH1%)EmisEoE-2aX=8(q6N>$DJEy#W72+YONeOKLI_>8=-2<(uK(#fbOsKgd~H z$Cr>?Ir@1=3QY>`un&E`oEzY=)q$|`E#!h}@xEq0U)hdeNy@XsX?S4{$JgTC8dn$@ zd`g^RMdv1Kw#H&q|H+F`O7x&HV#m+S@HPU8j82r&O6M(I^{3P_HDRQgi;F`z!GdX= z7{Te6PQz!9CQBe}Kn^Ao*3fAh3B&=qfctPm(W8U@U-z#%a~v<#as)rQd2oRMTJpPg z^c^Xm_q%q6CfxOvd)Sv5p1nV_eoO}{02WkPrX4fZZ)C9DpBQJh`~2t=8#Ng^u8Wz+ zD$k2&4e${H0;a^N099#GToAWH%5un)kAH8D;_58i?YCJ)Pq3!?`{y^)#MK^A?&Yj^ z$P$`2yi{vgpRu%a`b~PBT&jopQ?D0|S>P^h?1xuYKks68aJn^`39G)9?%WNVpbbde#xtZChh?B>1-t=qNtD)8Nm1TBM{9w~)KPu~3 zP>r(5{dKHMSRm3UGn1O=px5r~ea<$GjiXSm&c73&2|+-g3d){smdc z|JobKx-NXqF$*5==j6Wl-K-Aw?C!>p)foPUl+iVH$bB6tb`S zFV@;drXHRGa2^<@Y6Dd|soU{1=Mp91DQNz$Y9N6+bi7M|t&`r(h4ahEsc47#bZ=Yb zbLL^4+n^VQKU}6IxZ%&r%M*ZHem!s(vs{;nRko()dI1tZIJJUh>;ML#%Rf=`_m|W^ zEev7+2liUWrSwR$7|)I>F=mgRbvBvPAiIcF>mp*OPV4Dx_^!vv^f{`;<`RfYN!9}2 zWBUa(Gs2t>mtgBw`y|zrA}ysS-iv4RX=edz zG7sZTxGi3)B&DkSH`nPJ)kdY#*0Qp)d$5%F6X6BrL?0wFI!S7bIm2=oWRlIt&8@V8 z&MVfZki@uSr@H8g%Uc6`r*wdbjAI?-1FufS&H9_UMl?;*c|U0#0K&XiOT=l$V8qDaG)Ir<9zjma=n&Q@_4Z4+~K14(P}hnfja$sXe=< zTK0Aj*ScyAMWF|La`>aiRPQ4*>&ZJ^8cRWF18 zF0Nc(nx*SV8SF;gff?>ujic_T5g+t;yMvCP{mIvRJ{XL4^!N9hN{F zkbWS`qzP{8Gy!1S?RMRwoo+v5gl`qMaJ(fr#DN3Gk4eW31hxEKiuaMeDCbK-Kf zI%fy4-G>)Ys&vF1JGbJI#%*GT`^BZ_O2ly&b?-gF_?2dckdG?gfWhNxM+HaOCkStj zbpN9C{Xe}}Y%mhp!LJ97OL{=$NOz#L+DF`3AU&neKPAMH5WYRk3VT=xi%3L@#&t? zVi5BtN--`ReGOZBbBfq3t)UEbLi?{F0s>*Th5)c@N*@Ow)>lA)nep+euH02Zkfwts zuiHk;tvTe+K$61Pb)-(smY-Z#o)D+6X*>W(@*x5-gkBeW)Ji4FZP&SV1vwagJjp0V zcdowl&%X43Ju{|Fuo@>~_EQARO5z=_DWc8fu+YQ_dA&#I)wy6l$=nQ7;nPKrr4QKI zAKSm>1PCyu)4HHW3IADW4CQueNPKDAU@52T>R?j>ML<;hZczTnwVWvg3zj`Vm-F4j z$V$6*for0^P)3M3U}7WQ+4Kq@R}`}zn?P19($WX9xhJI#A1I4axnpwoiUf>sjpWbj z;$2go%=LU0X|ZGgT_y~YG@!`U0i&hJ5=uBVc~{sJR&wKV^FVZF!IFHOf$u&ATpuU} z>D!a5|6wjh1d`T@<*gotADkqq9qu@*?s9y_Z?K-^nhIV{(8(ppbex~iz`L2FLc{)W z+2{1eRM>cuZm2huECtpdaYL6@ft_s>frPl;hZS!vaTpo zy%ldw{F5JM8lmGn5MB1uvMJpHoR#{I{1f4qUqR5pSF5>F5hS0c_RZ_8FOKc@bk#`y z1kzNw91Wk;$_~u^*6#R ziTp&C=HyA-Gx3S33Ja%Czi$^8KBccDk$TD>wG~R#*K6i2nqgw(%A>-UaK97+REIe~ z+S+o`39^tv;yGWKo}cMEw{}T9YfSv=n%eZHb#p9MnQN}H*h?s?F>Kpz!wyqjNGSAZ z^+3?;x|%6ylE{*qy2QFq`EHYa*L|J12_qf)tFIVTFe=5c*G_Vve(Rv)L8xWmXX?= z`7TG*v*Y`it|0Fr?Ceg_{a{}X-M_!M0Nul@^k=ODP=JSXohzofQZh^r74vx3?j8uS z6Xc}LEj`c}`RVYX>E#qgt(}-phZ|#)0`Y%5H2stEabw7vX;y*RvQu z5yox!K{b#6*GzV<8%IOU&M}kf&NP1Zlj4wkCcN0h=Fu+gwv4=IQeC;0D&I3BHe-Ix z5N+i`Xu9$5IJA874(oR=`9R^E;&V2e^2Dv zYxStY&U)DEC7Rk#5s|T};(V7hPUP+a(1}AEDoERQAJbo4DIJg^zG`==F@+A`b(pCC z>|k-DGuU`Hlr?1OF$y0;R*^T~WCnySDalb5zlF{-(7ePxP705Vb4dhgIv{UZl|c}u zm*e70aA5T;`l=pG54>3uoGzf>u5>0Qb1UvQy1V=mRdlMmvHo6Y>&fm02Y!GSHOFAA z`l0yzvZ_yG4>36Oo)P7Oy}r$@+;Rr2-WO$lv<%!26R%jsuUNG@=irA#-e9J7Pv_nH zWip~Zj{J&aYIEk+ByoY!XB%cSi2%ok8t{b)f6Q`_H|uatFmRlBiuKcpk{krPG+Zm5 z0*=kNcsCY+YgM9&KWrH;!%pQS;+vIqc}UFxldlpwwhg;%dUJ=ImoM4#0MB%*V-WZ{ z*N0DCgLCK8IT7H_xLiqs#Y^C$7VwdR%vKqIHN{hUlu>*G_fE4_-Q%;mMKM<+NbT6ff0gRb!aVQ-1IciFkSnQx*D zG?`|d(=!vlNh`<2J^Xix)l^0JU=w8}Fv|0#sd!5aP7 zUpOq5KmJWT>l)i&EhX5Z{xs66roCXP# zF*r)k*J0!fUD;|6C;L*lFPnrt9i6$URqjq^tTXPQ3V~QWvCJ{2{MvBY!$lq}mdsP8W=Ik95q?CiN6po%nm$lC%h&n&t>BRVbh#V72+PNV&#sD(U*3z+>`Y4_qy6Y{jl!f1 zqM%uDM+oeaSp3Zea3XWMCi)W6nMn!J)GTbK8q;BzKFvp>qw*%wK9N-*>PX<$(#>sc zp&yU_Dh#%5|K zZpo3o;%9eopnp=EMSSUt*8!zvHY`Xyp0Br{b-DNH$rbzGhrgR}B)?z|i(Ewk+F@N9 z2}lw0kMXP{syUhh2(-&Q&mRwqy0EVh=*cZEr8er=75=XBv&_0SAEYnEtHb#lC~j}7OJ-#?Mp0%F#uj#L=9SVUX5nfeLd zkr^a_dOGhVx>s2MIu1HEiN6hgvyq&`2BhXOi(E_)4JCKT``m9oro9zei7U*U1vb#5dXrT7oF>MhnDX zQ%CnDueujJlkMl!6JybkW%#J4p3tKqokLL)1ACJFI^*%4+0!_aqs9~y2`ZM`B@%RM z@a_m*N}vwP8Gv6ztGbG43G!j9r-1HK)@OAOQi4lNKywR7p-R3^gy_e|;et0kdEPKr z8B^d6J{7w`)OD}6*R^V=;$Z&$_?$<8uH{IxHgJ3x&zaH$JrI_pLn0=afZ_x)-O(T1 z_Q`Q3TI8JE3sH)kUq4fVZi8rIejRSAL75G-0R) U%*WP;hf!jYbUU--;l5wKn0~ z!U!5$IFGHc+x;L$%GiadG@{zhZhyV!N(Mb$TJ9HVg+3dFb|Ic*&`x5KVhz8dx9cN} z>$5U`A@cYK2wkS`wn3lPf&C9j=yde!6X@s8gZuC~xb5oHonpm>mS-bZ!=lhXGzWDS z@+YsYwW%K&%AW}QDW1};>lj*$SXh3JkFYcb7y)qx`HqfzgTj0um($11c+n&aj;q}YsmEwSe z=qwKrwR_TUEx*@u`6@M-)gDopwOuQ;a5<@`n^!{?`%ZQhHsb&6WrYTEeD`-1%1i^h z!t0f{FP0u^XLwRKeZ*hl$`a={Rky?y9RSxdV-91jPEG>QSE@BxF(C#Mkv==vVfKDcx=r@$Glysy|)>jKBcHJUMM+Mk;%c9J7z8>v)8_$1!X{XjNX1 zHGGA`{?D(U@|=&opD>?rb?N|Bb8OEvnN}Ml>5wA4@UQhL&8vXqQ?lHAl4oz=z+JbH zFHCck05#B#`FwgKHN0n>luX?o@x47ePakZ_5=g2Mg3RpTAdb1Ma^Q|-up6u-;oawK zp!g*d8u5pou_2)K{-_?FsR!_(ESz#mZ)f&B8 zy6EeY);o5RgbaEEjz|v!&|DcVMFNce(gtrEcY4G1F@PT(;P-sl{IU37d2-B?wHT_) zb$kwG_jlRX=Lz<0Q#s^7<)6E%UzR{dhk{gKVL&;oGp3>bH|(xoNA=vXYL1CZLuB=< zy=1P{+rI1zX`EbX(>Do{g?p4za~Uro$B8WYcQ98S-mZ=NPCJ_39QoB6*cC@M^D#0u z6M%P?BtQ^IYA$Xr?(?Q7C@W7MCIzzUd}=-vI_K3%t+AHC)c^*xuhVD)Xtkgf$$cC> z4|MFT-|{zSmElme+L}DI8_(gk=$c=?MNCjZPAXl5E`IU)ryPw#sY>RN`D3o{i$jY) zK34YaG^{Dg?K1@tJ|B7+e5Y)>ggO|w`CE#D&H#zw)k`;hR{km)ROIeH=PfC@D7#!C z)dfH8B}(gMXI8!CsWQ>P=0l>Ja7Db3e3qH{X}I!K2`h}iM`+M?+5Xh_!QD0^WM-k& zhPNUM^Ijuf3hr0&{fQ9EM_U0{V)1y8r5HKAXxAISd)4ZQsqN#~q6(3)7jC2;9S4cg zp^AY$`5`RW8hQsMXbIP)d4Xl29E5oV2_ zKKc3$xGqNR!^-`$RP4R)*bxq4miUI*Fg zJfE1yLbe-wQ_bD_ulGfMh(4FAj}xQ@j?#z;$0_8|U;HBpi}smJ-M{zC&NJx@otC>Z zPa+kZ3Cndj-?gdD>m9D{2QN{+u6|zg5VkF~%sH0WT5dUpm44~0!EuU@J zxtGh*^(TDlm*{}1vYxD;W~6!$1ik0qDk?h-Df;6M|8` z`uyv|5`&A@*B*-=Dkz_UoU;y*UIQ?_4>vR;U)?0t~PgZ z;VCgRC58DUmJjA9KZrSkyIgHw{y~0)lJ)I25O?Z1I&E_jruBg1yYiknV4_^r)V4`s zVU|nP{_qwM&hOufG>Atr(W1-EBg?CB{6QfU)$4Gq#y}Bi=T!ZqC#Ps!h0Vp8sQJxoabg0@yPL+{EaHf1g#vL!n&w zkDD3?U~ zqv(t0T0?FcS~teN(;2a$1;pk7FjN# z8mgn+(VSb&{o#SZ_vur~tEZ>K@|OH{c`DBlpATK4A|CSYk&l+JXs79*K{D9IvT~e_+xy1r%H<-=Q2GDcN=}e>G2=(;kR5Aw9 z5^zW)`3EoSds+zA$z)rdGaXh|)(M1UF2CNwo6Op8xVV&5@$|m(thpg5S}~ohM%8Q7 z9{&vfy?S10FHkXf=qE?cnW0J1haQ8@ptN?sLBE0J#*)hY5?+fACNhq6Fyh6``jC8m z=1gBe`7Bo}`SkpKi`Rg6h3@rGU8`=$UHX#Q*khh|a#3G`u2ol16)$m@i>14bSzc`q zfL>Ci_ju%<6Yp-vL<_(Ot;b%q`(aCOgwgprq~=1P#5nV8KE;*XxCVOgZ78(uNdu!# zBIjwkv;NvqZ1Rb%;hFuYp#3DHbT$%Wjx5v&haCoZ@bImS_)!#Iu7h1)z3w>wX$|>R zSPtYKe`Q-S{`=ULXMXPBC>7ig$S>%rHdJtxI(DOS{;)@!zasVer;S(7KFm6qLhM)k z6>4IQtl^XBFLUllBvO`{dY4x(;mydcQLvV+b&kfOE9sHPXztrklmx>8e-T=)t^7ub z$Gy$w)B^7c@9}T#yDi>N`O{}}wjUx+J5?NJw)VQAbVBBU$^MPVuTOqlalNV>U#!bC za>Xn|^|A7IW%KNgzA_;?4-e{zCok1toDC2=%$8Iqzki#kvSm>u(oP7tZ|c&+gt)Cm z3gh;*%3d!hRpjC-foy7d$0T}4#jmFw*VmPi^lY*Wa4fP8eV3sQJ!wALYj3r^9dA9=)O zWfC~XuvuZZGM}yMU0l6|KJWVnQ2iK5UFT6ker07H;oMSt#D zwzlEBriqPy6m8I)Qql^P*jFOG+26ycjj>dz}3kN z2^V7&XikW{4dUbfJlrG`srG>;7f2jA3X(J;PsknorJ^HSUe5I@&TO| z#np$_%QRLK$y^_+4>rrdAhM52+OnziuZOB5K^{4TWDrtTQ#O^7?9+U)1@HO)0JPiy zlvc(S$pJ|OmiKX(#`GJM`>L`#*E^~26gMQGR9h(uUGuRY3u8n z4`n3yX=4{zlXnfnWnNYas`j%_rCe!K%x|F2fAso|Dd%J+N8%yyLgmMd&Iz2GK2c|y zf{yzo_~}dT&S`S=6-_{{YnqV+Kx|S~fdDD+EQx+q6wjC1IL%B#_9uc!{I1v-cvm`xdLd4XgP(-o15sZF83FD;w+C z(?Yk)&6FZymgI5cxd|p((Td1h4-$JWNEYRJ?CjRvRvBhv8-C3164;#aBm7!~x>JH?qHz81al&;4TJ zOyU6r2?{YTx36qOqwktM4c;N>4G!~@g~S#=J_Nm3J37%Wq64L; zF5K@9{O!mT2M&jVxikr?>3mQta_HO1(Yk~gC(pY%{Buw5xcqju4UPGp7JT`*H)YQV!Hr$8A}2D@aMb@^EP9|HWV=k!t_hKpA-nYuyBr=I z0AV<4|GCzx=Q4}`Q+q?#-R=QG?Qtg4`lK^3$EGT?Gm2384bta@P{MB$+ zRiuM~?=*<;;ZVxMop<)_hBw-{iHm%@ zTUd?qMZ!pq1PAwwE`gtR4XQ*Fn-os4jIJr0|Y*No2ahRoIp3~sL&wJB5TkpT2{It&cgjZ0? z%+!ygzaSca%nCzeW0-yB6yGmgYw3P@qx`G-=wIdcFNln9+XgKFlXw2@okm7u(l>27 zG$kGQ@DCUc_tb6h?j5^t289PJH}85#Oi~39EmeU_PK;ozBe>!u@!cm#?awzL;ALUU z&ppAB{j0(XQ-~|@HS5S0QMg{5DeKY1V2*z}pHDcpFa=j7kkx8&(j-jYQc|bsO1UM$X)= z6Axr!Tu7FC7Z=grH(E~i4#S%#{_H1Ak6W+n9#=+Wg-Y$eitx9!V-(;rNUp7?(~mKkhz+f#Gce|5&|%6IcBvvh~s6PzS;ZeDjQHxO&CRuJa z>o=i24{0w?3g}{p-9cwe3r#j85*{A6UZ-^}JOjf>(hoDQHm0IXIus96rnVHzdh;OF z9D`5VdYHn4w+qe}IE;Mt^BPgzDJZC0JtwOC%2i>wcJ%VAd8;ns1Qy?OiCCu3^JoPfu%5W=yVg0ladP&+s4;LlkI`t_q}|b{B%++V zKK>FUYDI$FT?1vKR+9_0WLfKXBaU9(sY~CS#vK!nL=bI=_O}F6C_CfrA_vJk2hS2H zQ5vBWmUJu4^7Wt)K4c-8f^IszE{!1gFHTG;=eah7+W9nAW$WnMDm{(4}$k`?-oWL z4Gq^&pq+fy3QRt+C*}DdRT=LVa>hCr{w~GkITR#>xXlBXK@h)=(^Y6EL|Ah=(QoN7 zaN#I%@H!%2f2`@oLSZQ1+uKGJMZ#p<9VBuf&zuj}L=chb$OV)h@CGG+&XPGyZOp{SP4L;l!TzcUd7js4Pjc*B=canU?qnebw z;&_y9v>g&dci(}q*@W!3j}Y$dBzjuFbha{Oz^^@7DlV|8^oH%)Wyaiog3xZ`k<$b8 z$BX_H$wdK#sRS!J{R(=~g(k&V=o>ERc6$7vrjRq}R2M}B)R6GUO6KJ0hmFvNiV8)e zAs=etPagz52g&kJ0*UZ`Z<11k;>?R{IhYXBbm*b^A?gtHy1ih#GagPqmY_;k!V^`U za>u!(m+;w_WrI)qCDIiW&9=WG?*m2NFuSs@EP_*3oO zq4L$1C}8&X+Bb9K+|@^&W$I&^0L!2KapRH^PUh`}Be~-~ddKOPR^ zmFROg{6EfC1v+S1=ji4HZtt0gcPGU~76y;MXAV(0&lb-psCoEOtofV^2~?v&Hs8s7E$3Tx=!76zn73OpOh#&>ON^B3UKTEP|oc!5zM}5#=NtKA>yf!WNR%Bjp^+s7a zL3XhYBd?fwigpH3(!G*_cIqQ%-@uy>1-Kz0zAnT1Kj=BnGF1(93+gs=GrjMy-Yl8) zv`aP;%u^Gpc=cL7xLDL$)I%BmSzMce0Vw-EF`FeROFrpu1Pq*w?z}!vaYN(oypv}` z?5WJ3(A(S@?-+w0*bX)rR9bP>ZK6=Fc|fL=iNbCNJqje~E(Z;sd5fGz z0b4YrFh-BRSro=yOP`Ci?A65CV_eSd3Ol+;O1&Qy|5mk->isU?Gfr`Hj z9)7uSF)eYyHUF#LBQck60iUi)GbtxMQJuAuzzs($6^5x-CyCA>Ond3}61F$_AD;1; z6K(K48uhcQh`^1$OM=i9C4xHn=75&I_l8eVJ<*b`KU@&(E&3X19a$HXaKH2+%F#N# z^j?bFUE6~=v}qy~(zG!rww%q)9Qgy9TCF-p{1jdtS!e@^7Sa@^H#Bn*r=y=gs5X8e zw*EPtr3j_p6zyne)r$Jz*c2Ty*IpmCXNXvD{cDfEPz^Wh<2ezT{3F7G0x{Q+n=&zg|SB2%qDtnAfwfwYB4iV0|g_x z7O7wnskAYi4+?{Q<~GA+R-~(7-C*fER_wm;rDX{TWQrekio#SZ6iBH%BdqM|W}vk~ z5Q?BAmQT*$`c+=WpP(Mbj+)e0SdaI_ZMi$=K8r$6S_9v`F?DgM8whKX8wKrlo+cU0 z@F{8E?V6^ zw~?QM`qXn2?$B#n`ZA&)+iljpbk7aa;$$GGLNOZT`7?ek3|0HBY5l>BroEb+vl$S) zYk^;2BpjgejLg&fBFg;Yoj7*9XhK0GrD>f!B~QFRw5?eHCe4P(M#e)mL8wA6;{1NO zG*Uzly0{rx(%b3~v5vOMcEg*Kw>IlEb`yhr7HRSY?rF*O^Q4DHhDg#C*o`%LGf@-e zq#u_)7kg~`f7zbqefU|~{s(06qg7@)&4>!QX9O&lm2beTazsek?#mMsqoJJ_>~$Uq zT(KdR?1-qByIv(n94_f9!$Qtv-hc;XZK8m!3~)>KZ6~*~5F?>b$)u#}3(2zNbXZLH zGH1rx<8J(i;eC7s+18-D_bqJZAN?E~h3m=GOH604Q^~uBPZ+S2#;TLdgG31Ni!NlD z3PxwsOxcoFW$vQu&aGY6GYg!o?w-wc6GjxT{r);|Pc$$Tr_Zh-op&M)Xk_)oD_3c^ zL*Cq(R{UY+?s`fLjOAjv&#DsdE)hUc3l#6S(*Tyb?QOQ1ix36I?G16=T~MZ8NZsp` z$$TbQX73asWklDX$ZZ={$9uUNrt0P<8rW$;VQ%@oYkP_%@=U&Mniq*u+mJa`bcve7vE4E73W(X&c_N@;R@|bbi7>%!wkCqaw2mu{*`L);OOkRj3y(HR-nz$uZ`zy}*xmo91bp*BH^iv+mz@ z7((KOi>)jAofaS5HcptSFI#XGN= zm1!BCIk3%{#2jO#QB@=Ex~<#4GImFj_Ad?UvU8=0SL8`C=EiX!8Uf2qX8J!qi^{!A zO0RB8NxT*v=){$=yM4vRX5x=f=Ah&RMKJ&eI0Oj{zk03wj#m>CMcz8dW~NF;RJ#X% zR-U1$x+C86{SsK}WpK1$3gf*BK#3fLZq;hrbr>l3e3~FamQkdgO(EHc3`4NGja?E2 z@DD=rN=m5;{yB~?gBE9lZUTf+_$&(aaGpsA%1Gvi(8t8^cnvBp>k^1SG39Yg+zBs-2@wS$C-o)W-tbC-&jFUipn#4%w z=JViYyF}P)-1RgTHQ5%e@xW# zaUbh~ezM^#^w1b{a6&-^|Eyg#g8T%%pUMITS$jcp&%l)|QO(Ym+}lWo{BW z@%JI5^<6nQpp_SfUfY~Ge70U^a1B8HJmr2O-b$4F+10_AY@HC#x7n-;HU{-qCLXHp z)Fb*EKY?3L$v2?AkGI=S$(KIa3ON|cX)?_+7X?0z&A*DhI`dmA)2ome-a1<$hDu?e z+#hNpx^Fz6^RjvJs(CVNdh(e4S8m45NpiCxD|#p-xTF&SexjUUkFovYEhm6Dw})g8 z1}By~5SjH_?ro@Kz^pnS=QnQx5#MJGl>5Kb2g4aelZb0wi#~OZ9wHQTh2|6k z*PizCT`=W_x(wdFMfoX(o9d67>}NTVjy}kFK6Te7N4!F@T>xAiU92@=g>UjXQT$~WTpqY(U-$XB?Z5441Qkx@Yr*`62<0cVxi3Ab8m|n^k#h2R{V?&BrZSw_!9b7g z&ePeyFM)xr8gcAL(~TGGy)s44*vM_LEnx_ zV8FxQbwx#nN zj(?CUfdRn-JfS>*9MjJAvBZ&~A7a`uz*6{O@|z6kK=R)pd)LS$;xKdu0Lm7fKwg1#?W4S73E?S=Fdm>&owr!pp2 z=dDA5`}<$j&hEaR-M$GPAHI!{0`gomQ^(c5K2@Ag)9{wI+AMQKz}bt>!OWDDaHfmW zb!0t#Qk1fMkU=im=gvKCRVbS=uC{DxKw8=xzfK!p?=%N3dSxSu0csGyu=~OkG?PSX z*STP+g6*GIO$H9zzB>165-fQLc%&F{!TYzq&I$i_U)u;~Qona@bRxbzs-u~|U#m9E zttOgD3xpZ;82aT^JSqG*bR;{sQA7Aqdws{r<$k_jx_0D(4j+}WrvcVgoGT2(QWf;i~Sm*(;qA0X(Puk1?6I5(*b4h%jBBDmEi17s*KV!kn+?J5a| zWD!e1{LE{A8|yZ6;l>NfCbewPK=*N7wOPx%`16YTZ50y5^Ka7qTm%5{cL@K-PU)|z;Ksc zM5ef6EIL~p+_o_!#$MRZ4Zz~Rq!%&72NV(#MtXzL*b@t&HaQ&4l}ZjzPvI)&8^9Hu zosZw6_N>kN3z83+@KD|w*bDLBEka=VI}Cx8$m9SYMW$rf{js;&+==@Op=M+^SY7y} z6)SM+K&kz`j@Ojj0s?_{%O$NFOc>M60id;!^KGL!#4&7OK;Hti@}@ji8Xy+}?TK6a zn&IaQWD1>z+cw-0YPCnoWL8*m!p2rl5d2O(MOgoz%$AIniZbmJ0n zx?AnPeRrn2La?55y1dAIgBpG0|2vLntlN$ovgeS`K`LVi~;P2Y%GnhgVZ`; zm8M=o6QkB+E2>J@{LA?87$?F%)eWZ+~kDpP4c>r!1ZYGUz^pLR}LOM@vZC2|!X+=5Wl{X~Gjf+uX z<`3x4#R)`#Ql#<$-c$1W3dZ*3EC)K^EIKEr-IWt47_+PEd)us&0|v0#u0Vf100_o^ z<<;pTH9J^>NI_G#*%Unubb^Kz06 z?0N8Bebtwi*Ygx}4m|w|0N@y#ua)opjh&-wKs1ind0J~&R}^VuSR!eSE6>8c4%%m& zVvK2L;;wp8u1roO99&`~!MuiWd})o>QK#34@ZRwwL&FZ0q}xA_F(ntsrJu@6wr1a( zfPUG%f-&e#zjG7q5;$; zErN9$2u0}u?VN!?Ue219N1sg2V#D`)JsCXBqQ6j_sm%F#-m;^wiV{NDitizt*}S+~Toqf? z^d>&NE0)%Ev-TUqKVH~5G!V+{en_u3fpGlTATTm=CVsT`j2>`zK71)di{HOLa`8AJ z6FOk??Pm;xqy-K7czjT#1*>mP_j>G3$H92T*)=^xS3L=Yx`BSDiP=p>w|_i^C)7AO zNCOoTb{V_dv~_`l9e*f1=YX?MWVY@my4jyt&GeA{Z$i{tmWiwdyF_7bVzy1Qz0Nkp zi?vGGk(J_sJX2JY4{|S^udX7vk~3~AV2<_N0(w%^&{R%hy$C-CJ10}!;)WA?TIugAY}YYHn=#ex|lamns( zs9`&K;F?3;%{9fm$qL_zG~<+siC5s3M-YJJOFZIr#ZM3{@HvK&B$uxPtr&rl^v!?) zDCv#&k|-FBq1NG0_yZ3MnSb(DEkTakU?6;2KoQsQQ<^g(+t6(i(-pc>nS}kZp{dXG zSCpZ_IzPQ@cZJYa&FovtW7lu;;lPlZ-Q<8Pf6u$8@fl?$ z-xX0BxETshX)b+3dg_@%tG~Q`nu3VP?j4vGu|y)6?vY|Iw50;71cY6|Z`Tu2!vqop=)e=^g}a)YGsDUi*33t|rUod(_+J7=|EF}&5J)Jd9(oM9fi8DNpwyx;JfSr;K>Q-O9 z^vQW`-Sle77(I&`^lxV(PAeBB4HAME-(MJBZC>k;FZQ0ER~&f7GK1VGzQ@JLDZf>v z;<7N|e^gfpkYOLwFa6faH&&q!Sz@jstMQ$MtUzC=C=YN>=4BC(3vSWn1}5UASa>`y zXJ28Xu)pT8+lY(vfFh_BV^YcsqN)}QxlL>+-EWnONzw?2=KR?qWqa(7V1r0C=7s0K z@NQZLw-d+VFZVIrD4$CSbCbo^r98AjNG9dVWQ7wFDB!xu((24{z1)TfWm#Cyye$uh zqQ**!uhi;6l3-(UFLyCzW@A;5pN&5!QxW!6Pwx-i?hcv8Y46ag%SN9276Mye^z6+- zw^AyntF%|}w(9xX2W4Owc5z}U*{~x_t{AE)GYyoKFgG`xAEEFYgIRq=8Wis^*l8_e z(MyLD5w>YsY{&VzGRJ@a@aUZG91?k6XX^|kh>K$vY1mW6sa0@B7fvwV72k~SHnoGG zDPQu|gD3HRMS(Ap9~@YAV=XhV(H@hk!(jt*X%#MusTjDZ5ISlDF2m=8Smg^aQG6Fa z<|@E98M(=3Kcr=K^>m4xG$lyArp)A92@Jp-ewHv%h6j?>IY_WULGQJxo7&f$Tg$#N zHhGZ{yZHOGy0Xq#J2HQKg9(uZCKI|QRcuuR8S-V%=_bF+{x@>>Wf`Tr_hSIguKw9k6PJR5ifWC_u<*p z@wi1P!kuG73zI~^7ani}Ma)z_!(e)swmg_r{?+&5ce{Upy#qu3<{YU z=Uw?dDaM%V2`+;J%e;Woy>I2SvXUWo!p+>-Pk;U5A#_0xgjIz z*QT53*WnicVCs2pT<^CSeJX7-_PTQqY=RYVk<)o|leweTE~h5Gpii++If%;_%wmi7V4~+pQ8V5y4sW8mqhL#_AdyIs zU7dDp2bpg})ciq_%$ISC;YEJJ`{`n=wuUsARLs&8Z2ajy+)n>`oQ$RAZ- z0zMg8?UDaD_a82Mu|*GXdY3j40zc9^^(5MN)OO$G_|7#H01pzJU-n)|8+%|vtV{Hd z!!7p;xbwUlBd+V%e?HEX<|VV9wliWqEW&@jZ4ND)IbNJ-erFxCWgcenFCD2Ulu-Ix zw|WcQ1}IG%j`O#tSS2Ucl?gEn&b23hxggkI$pI3HOq^q#9Mumky#ZK{xVQ^;wW)ae zXZ%u#7_nd#;?)75`;8@h16>tP5Mb>=+#X^_#(!Mj!MT($p7!Z#@bnzdryk{%f3;tE z>bw~2mdoq1*|7PAQ5_IR9FXY2YY-EB@z56vXP0{lys5o0bbG(YqBPgJe{A+9*Ltip zkQD_bZ6blKuguFZpblnij}>+>2jv=GK3p7Ikc5Qb=T6rAQj&zKKWYcC?XKC8J#V=46NwK#M~d_DVqt7pxV@G!CU{pAcR#Fq#oK z&kz8|J?<3;9J`KQUk2wUn8%mRXFd|*pnAt#EDSp|Mm_G{R=VzqSY?gKZ+oSE^z+n+ z{a+L~cWa^%#6I9Vd%1@sE*|ytlGyxXh zYOhqdFN@Hmfxi)}>3Itz-mB%PjU&i5%OCTa?2z{_%t?|P&Q(;W6eEL7r4zx;Wg)fQ z^e*1oEAQ6js)2#qhjQV}Ev_MQf0IEU9IH30la?41_(lr@pG^{|iPv*WWRnH;+()(B=W~tzeZCd$% zNB$i;T%-Qd`v30zZ}q<;{_hG8@c*WNYyN5~{{Qm7KB@n^(^QiCUfYQ_;NqilHk}}S Q0{{SkplgCF!#IZj9|Om`00000 literal 0 HcmV?d00001 diff --git a/taiga/hooks/gogs/models.py b/taiga/hooks/gogs/models.py new file mode 100644 index 00000000..fca83d73 --- /dev/null +++ b/taiga/hooks/gogs/models.py @@ -0,0 +1 @@ +# This file is needed to load migrations diff --git a/taiga/hooks/gogs/services.py b/taiga/hooks/gogs/services.py new file mode 100644 index 00000000..40d06fab --- /dev/null +++ b/taiga/hooks/gogs/services.py @@ -0,0 +1,37 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import uuid + +from django.core.urlresolvers import reverse + +from taiga.base.utils.urls import get_absolute_url + + +# Set this in settings.PROJECT_MODULES_CONFIGURATORS["gogs"] +def get_or_generate_config(project): + config = project.modules_config.config + if config and "gogs" in config: + g_config = project.modules_config.config["gogs"] + else: + g_config = {"secret": uuid.uuid4().hex} + + url = reverse("gogs-hook-list") + url = get_absolute_url(url) + url = "%s?project=%s" % (url, project.id) + g_config["webhooks_url"] = url + return g_config diff --git a/taiga/routers.py b/taiga/routers.py index 66e1b9f7..24974b74 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -208,6 +208,12 @@ from taiga.hooks.bitbucket.api import BitBucketViewSet router.register(r"bitbucket-hook", BitBucketViewSet, base_name="bitbucket-hook") +# Gogs webhooks +from taiga.hooks.gogs.api import GogsViewSet + +router.register(r"gogs-hook", GogsViewSet, base_name="gogs-hook") + + # Importer from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet diff --git a/tests/integration/test_hooks_gogs.py b/tests/integration/test_hooks_gogs.py new file mode 100644 index 00000000..290bc3f8 --- /dev/null +++ b/tests/integration/test_hooks_gogs.py @@ -0,0 +1,502 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest + +from unittest import mock + +from django.core.urlresolvers import reverse +from django.core import mail + +from taiga.base.utils import json +from taiga.hooks.gogs import event_hooks +from taiga.hooks.gogs.api import GogsViewSet +from taiga.hooks.exceptions import ActionSyntaxException +from taiga.projects import choices as project_choices +from taiga.projects.issues.models import Issue +from taiga.projects.tasks.models import Task +from taiga.projects.userstories.models import UserStory +from taiga.projects.models import Membership +from taiga.projects.history.services import get_history_queryset_by_model_instance, take_snapshot +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.models import NotifyPolicy +from taiga.projects import services +from .. import factories as f + +pytestmark = pytest.mark.django_db + + +def test_bad_signature(client): + project = f.ProjectFactory() + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = { + "secret": "badbadbad" + } + response = client.post(url, json.dumps(data), + content_type="application/json") + response_content = response.data + assert response.status_code == 400 + assert "Bad signature" in response_content["_error_message"] + + +def test_ok_signature(client): + project = f.ProjectFactory() + f.ProjectModulesConfigFactory(project=project, config={ + "gogs": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data", "secret": "tpnIwJDz4e"} + response = client.post(url, json.dumps(data), + content_type="application/json") + + assert response.status_code == 204 + + +def test_blocked_project(client): + project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF) + f.ProjectModulesConfigFactory(project=project, config={ + "gogs": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data", "secret": "tpnIwJDz4e"} + response = client.post(url, json.dumps(data), + content_type="application/json") + + assert response.status_code == 451 + + +def test_push_event_detected(client): + project = f.ProjectFactory() + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = { + "commits": [ + { + "message": "test message", + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + GogsViewSet._validate_signature = mock.Mock(return_value=True) + + with mock.patch.object(event_hooks.PushEventHook, "process_event") as process_event_mock: + response = client.post(url, json.dumps(data), + HTTP_X_GITHUB_EVENT="push", + content_type="application/json") + + assert process_event_mock.call_count == 1 + + assert response.status_code == 204 + + +def test_push_event_issue_processing(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.IssueStatusFactory(project=creation_status.project) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (issue.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue = Issue.objects.get(id=issue.id) + assert issue.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_task_processing(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.TaskStatusFactory(project=creation_status.project) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (task.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task = Task.objects.get(id=task.id) + assert task.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_processing(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.UserStoryStatusFactory(project=creation_status.project) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (user_story.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + user_story = UserStory.objects.get(id=user_story.id) + assert user_story.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (issue.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (task.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (user_story.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_multiple_actions(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.IssueStatusFactory(project=creation_status.project) + issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + test TG-%s #%s ok + bye! + """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook1 = event_hooks.PushEventHook(issue1.project, payload) + ev_hook1.process_event() + issue1 = Issue.objects.get(id=issue1.id) + issue2 = Issue.objects.get(id=issue2.id) + assert issue1.status.id == new_status.id + assert issue2.status.id == new_status.id + assert len(mail.outbox) == 2 + + +def test_push_event_processing_case_insensitive(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.TaskStatusFactory(project=creation_status.project) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test tg-%s #%s ok + bye! + """ % (task.ref, new_status.slug.upper()), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task = Task.objects.get(id=task.id) + assert task.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_task_bad_processing_non_existing_ref(client): + issue_status = f.IssueStatusFactory() + payload = { + "commits": [ + { + "message": """test message + test TG-6666666 #%s ok + bye! + """ % (issue_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue_status.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The referenced element doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_us_bad_processing_non_existing_status(client): + user_story = f.UserStoryFactory.create() + payload = { + "commits": [ + { + "message": """test message + test TG-%s #non-existing-slug ok + bye! + """ % (user_story.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The status doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_bad_processing_non_existing_status(client): + issue = f.IssueFactory.create() + payload = { + "commits": [ + { + "message": """test message + test TG-%s #non-existing-slug ok + bye! + """ % (issue.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The status doesn't exist" + assert len(mail.outbox) == 0 + + +def test_api_get_project_modules(client): + project = f.create_project() + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("projects-modules", args=(project.id,)) + + client.login(project.owner) + response = client.get(url) + assert response.status_code == 200 + content = response.data + assert "gogs" in content + assert content["gogs"]["secret"] != "" + assert content["gogs"]["webhooks_url"] != "" + + +def test_api_patch_project_modules(client): + project = f.create_project() + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("projects-modules", args=(project.id,)) + + client.login(project.owner) + data = { + "gogs": { + "secret": "test_secret", + "url": "test_url", + } + } + response = client.patch(url, json.dumps(data), content_type="application/json") + assert response.status_code == 204 + + config = services.get_modules_config(project).config + assert "gogs" in config + assert config["gogs"]["secret"] == "test_secret" + assert config["gogs"]["webhooks_url"] != "test_url" + + +def test_replace_gogs_references(): + ev_hook = event_hooks.BaseGogsEventHook + assert ev_hook.replace_gogs_references(None, "project-url", "#2") == "[Gogs#2](project-url/issues/2)" + assert ev_hook.replace_gogs_references(None, "project-url", "#2 ") == "[Gogs#2](project-url/issues/2) " + assert ev_hook.replace_gogs_references(None, "project-url", " #2 ") == " [Gogs#2](project-url/issues/2) " + assert ev_hook.replace_gogs_references(None, "project-url", " #2") == " [Gogs#2](project-url/issues/2)" + assert ev_hook.replace_gogs_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_gogs_references(None, "project-url", None) == "" From a9d662412433ff51d1e2a2d3a9e8329570c7d044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 21 Jul 2016 12:58:10 +0200 Subject: [PATCH 121/261] Add changelog entries --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be370f50..b2890f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ - Gzip export/import support. - Export performance improvements. - Add filter by email domain registration and invitation by setting. +- Third party integrations: + - Included gogs as builtin integration. + - Improve messages generated on webhooks input. + - Add mentions support in commit messages. + - Cleanup hooks code. ### Misc - [API] Improve performance of some calls over list. From f66b4c9640fc8425504c6ceb26204b50a21cb116 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 20 Jul 2016 14:31:48 +0200 Subject: [PATCH 122/261] Refactoring bulk update order API calls --- settings/common.py | 3 +- taiga/base/middleware/cors.py | 2 +- taiga/base/utils/db.py | 31 ++-- taiga/projects/issues/services.py | 15 -- taiga/projects/services/__init__.py | 1 + taiga/projects/services/bulk_update_order.py | 59 ++++++- taiga/projects/tasks/api.py | 89 +++++++++- taiga/projects/tasks/services.py | 30 ++-- taiga/projects/tasks/validators.py | 3 + taiga/projects/userstories/api.py | 83 ++++++++- taiga/projects/userstories/services.py | 38 ++-- taiga/projects/userstories/validators.py | 4 +- .../test_userstories_resources.py | 6 +- tests/integration/test_issues.py | 9 - tests/integration/test_tasks.py | 4 +- tests/integration/test_userstories.py | 21 ++- tests/unit/test_order_updates.py | 165 ++++++++++++++++++ tests/unit/test_utils.py | 17 +- 18 files changed, 465 insertions(+), 115 deletions(-) create mode 100644 tests/unit/test_order_updates.py diff --git a/settings/common.py b/settings/common.py index 333d310a..7f3f8cbd 100644 --- a/settings/common.py +++ b/settings/common.py @@ -437,7 +437,8 @@ APP_EXTRA_EXPOSE_HEADERS = [ "taiga-info-total-opened-milestones", "taiga-info-total-closed-milestones", "taiga-info-project-memberships", - "taiga-info-project-is-private" + "taiga-info-project-is-private", + "taiga-info-order-updated" ] DEFAULT_PROJECT_TEMPLATE = "scrum" diff --git a/taiga/base/middleware/cors.py b/taiga/base/middleware/cors.py index c7e2c615..3f5cbd38 100644 --- a/taiga/base/middleware/cors.py +++ b/taiga/base/middleware/cors.py @@ -25,7 +25,7 @@ COORS_ALLOWED_METHODS = ["POST", "GET", "OPTIONS", "PUT", "DELETE", "PATCH", "HE COORS_ALLOWED_HEADERS = ["content-type", "x-requested-with", "authorization", "accept-encoding", "x-disable-pagination", "x-lazy-pagination", - "x-host", "x-session-id"] + "x-host", "x-session-id", "set-orders"] COORS_ALLOWED_CREDENTIALS = True COORS_EXPOSE_HEADERS = ["x-pagination-count", "x-paginated", "x-paginated-by", "x-pagination-current", "x-pagination-next", "x-pagination-prev", diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py index 6569069d..9769abee 100644 --- a/taiga/base/utils/db.py +++ b/taiga/base/utils/db.py @@ -17,6 +17,7 @@ # along with this program. If not, see . from django.contrib.contenttypes.models import ContentType +from django.db import connection from django.db import transaction from django.shortcuts import _get_queryset @@ -26,6 +27,7 @@ from . import functions import re + def get_object_or_none(klass, *args, **kwargs): """ Uses get() to return an object, or None if the object does not exist. @@ -119,19 +121,28 @@ def update_in_bulk(instances, list_of_new_values, callback=None, precall=None): callback(instance) -def update_in_bulk_with_ids(ids, list_of_new_values, model): +def update_attr_in_bulk_for_ids(values, attr, model): """Update a table using a list of ids. - :params ids: List of ids. - :params new_values: List of dicts or duples where each dict/duple is the new data corresponding - to the instance in the same index position as the dict. - :param model: Model of the ids. + :params values: Dict of new values where the key is the pk of the element to update. + :params attr: attr to update + :params model: Model of the ids. """ - tn = get_typename_for_model_class(model) - for id, new_values in zip(ids, list_of_new_values): - key = "{0}:{1}".format(tn, id) - with advisory_lock(key) as acquired_key_lock: - model.objects.filter(id=id).update(**new_values) + values = [str((id, order)) for id, order in values.items()] + sql = """ + UPDATE {tbl} + SET {attr}=update_values.column2 + FROM ( + VALUES + {values} + ) AS update_values + WHERE {tbl}.id=update_values.column1; + """.format(tbl=model._meta.db_table, + values=', '.join(values), + attr=attr) + + cursor = connection.cursor() + cursor.execute(sql) def to_tsquery(term): diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index 56790e82..782a184c 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -72,21 +72,6 @@ def create_issues_in_bulk(bulk_data, callback=None, precall=None, **additional_f return issues -def update_issues_order_in_bulk(bulk_data): - """Update the order of some issues. - - `bulk_data` should be a list of tuples with the following format: - - [(, ), ...] - """ - issue_ids = [] - new_order_values = [] - for issue_id, new_order_value in bulk_data: - issue_ids.append(issue_id) - new_order_values.append({"order": new_order_value}) - db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue) - - ##################################################### # CSV ##################################################### diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index a115275b..e8a93623 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -27,6 +27,7 @@ from .bulk_update_order import bulk_update_issue_status_order from .bulk_update_order import bulk_update_task_status_order from .bulk_update_order import bulk_update_points_order from .bulk_update_order import bulk_update_userstory_status_order +from .bulk_update_order import apply_order_updates from .filters import get_all_tags diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py index 48e85218..4abf0e24 100644 --- a/taiga/projects/services/bulk_update_order.py +++ b/taiga/projects/services/bulk_update_order.py @@ -24,25 +24,66 @@ from taiga.projects import models from contextlib import suppress -def update_projects_order_in_bulk(bulk_data:list, field:str, user): +def apply_order_updates(base_orders: dict, new_orders: dict): + """ + `base_orders` must be a dict containing all the elements that can be affected by + order modifications. + `new_orders` must be a dict containing the basic order modifications to apply. + + The result will a base_orders with the specified order changes in new_orders + and the extra calculated ones applied. + Extra order updates can be needed when moving elements to intermediate positions. + The elements where no order update is needed will be removed. + """ + updated_order_ids = set() + # We will apply the multiple order changes by the new position order + sorted_new_orders = [(k, v) for k, v in new_orders.items()] + sorted_new_orders = sorted(sorted_new_orders, key=lambda e: e[1]) + + for new_order in sorted_new_orders: + old_order = base_orders[new_order[0]] + new_order = new_order[1] + for id, order in base_orders.items(): + # When moving forward only the elements contained in the range new_order - old_order + # positions need to be updated + moving_backward = new_order <= old_order and order >= new_order and order < old_order + # When moving backward all the elements from the new_order position need to bee updated + moving_forward = new_order >= old_order and order >= new_order + if moving_backward or moving_forward: + base_orders[id] += 1 + updated_order_ids.add(id) + + # Overwritting the orders specified + for id, order in new_orders.items(): + if base_orders[id] != order: + base_orders[id] = order + updated_order_ids.add(id) + + # Remove not modified elements + removing_keys = [id for id in base_orders if id not in updated_order_ids] + [base_orders.pop(id, None) for id in removing_keys] + + +def update_projects_order_in_bulk(bulk_data: list, field: str, user): """ Update the order of user projects in the user membership. - `bulk_data` should be a list of tuples with the following format: + `bulk_data` should be a list of dicts with the following format: - [(, {: , ...}), ...] + [{'project_id': , 'order': }, ...] """ - membership_ids = [] - new_order_values = [] + memberships_orders = {m.id: getattr(m, field) for m in user.memberships.all()} + new_memberships_orders = {} + for membership_data in bulk_data: project_id = membership_data["project_id"] with suppress(ObjectDoesNotExist): membership = user.memberships.get(project_id=project_id) - membership_ids.append(membership.id) - new_order_values.append({field: membership_data["order"]}) + new_memberships_orders[membership.id] = membership_data["order"] + + apply_order_updates(memberships_orders, new_memberships_orders) from taiga.base.utils import db - - db.update_in_bulk_with_ids(membership_ids, new_order_values, model=models.Membership) + db.update_attr_in_bulk_for_ids(memberships_orders, field, model=models.Membership) @transaction.atomic diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 3dc2bd32..232d496e 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -25,12 +25,15 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin - +from taiga.base.utils import json from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.milestones.models import Milestone from taiga.projects.models import Project, TaskStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin +from taiga.projects.userstories.models import UserStory + from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -104,16 +107,74 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone: raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task.")) + """ + Updating some attributes of the userstory can affect the ordering in the backlog, kanban or taskboard + These two methods generate a key for the task and can be used to be compared before and after + saving + If there is any difference it means an extra ordering update must be done + """ + def _us_order_key(self, obj): + return "{}-{}-{}".format(obj.project_id, obj.user_story_id, obj.us_order) + + def _taskboard_order_key(self, obj): + return "{}-{}-{}-{}".format(obj.project_id, obj.user_story_id, obj.status_id, obj.taskboard_order) + def pre_save(self, obj): if obj.user_story: obj.milestone = obj.user_story.milestone if not obj.id: obj.owner = self.request.user + else: + self._old_us_order_key = self._us_order_key(self.get_object()) + self._old_taskboard_order_key = self._taskboard_order_key(self.get_object()) + super().pre_save(obj) + def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr, + project, user_story=None, status=None, milestone=None): + # Executes the extra ordering if there is a difference in the ordering keys + if old_order_key != order_key: + extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) + data = [{"task_id": obj.id, "order": getattr(obj, order_attr)}] + for id, order in extra_orders.items(): + data.append({"task_id": int(id), "order": order}) + + return services.update_tasks_order_in_bulk(data, + order_attr, + project, + user_story=user_story, + status=status, + milestone=milestone) + return {} + + def post_save(self, obj, created=False): + if not created: + # Let's reorder the related stuff after edit the element + orders_updated = {} + updated = self._reorder_if_needed(obj, + self._old_us_order_key, + self._us_order_key(obj), + "us_order", + obj.project, + user_story=obj.user_story) + orders_updated.update(updated) + updated = self._reorder_if_needed(obj, + self._old_taskboard_order_key, + self._taskboard_order_key(obj), + "taskboard_order", + obj.project, + user_story=obj.user_story, + status=obj.status, + milestone=obj.milestone) + orders_updated.update(updated) + self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated) + + super().post_save(obj, created) + def update(self, request, *args, **kwargs): self.object = self.get_object_or_none() project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: try: new_project = Project.objects.get(pk=project_id) @@ -223,12 +284,28 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) - services.update_tasks_order_in_bulk(data["bulk_tasks"], - project=project, - field=order_field) - services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user) + user_story = None + user_story_id = data.get("user_story_id", None) + if user_story_id is not None: + user_story = get_object_or_404(UserStory, pk=user_story_id) - return response.NoContent() + status = None + status_id = data.get("status_id", None) + if status_id is not None: + status = get_object_or_404(TaskStatus, pk=status_id) + + milestone = None + milestone_id = data.get("milestone_id", None) + if milestone_id is not None: + milestone = get_object_or_404(Milestone, pk=milestone_id) + + ret = services.update_tasks_order_in_bulk(data["bulk_tasks"], + order_field, + project, + user_story=user_story, + status=status, + milestone=milestone) + return response.Ok(ret) @list_route(methods=["POST"]) def bulk_update_taskboard_order(self, request, **kwargs): diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index ac7a6478..055583bd 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -27,6 +27,7 @@ from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot +from taiga.projects.services import apply_order_updates from taiga.projects.tasks.apps import connect_tasks_signals from taiga.projects.tasks.apps import disconnect_tasks_signals from taiga.events import events @@ -73,24 +74,33 @@ def create_tasks_in_bulk(bulk_data, callback=None, precall=None, **additional_fi return tasks -def update_tasks_order_in_bulk(bulk_data: list, field: str, project: object): +def update_tasks_order_in_bulk(bulk_data: list, field: str, project: object, + user_story: object=None, status: object=None, milestone: object=None): """ - Update the order of some tasks. - `bulk_data` should be a list of tuples with the following format: + Updates the order of the tasks specified adding the extra updates needed + to keep consistency. - [(, {: , ...}), ...] + [{'task_id': , 'order': }, ...] """ - task_ids = [] - new_order_values = [] - for task_data in bulk_data: - task_ids.append(task_data["task_id"]) - new_order_values.append({field: task_data["order"]}) + tasks = project.tasks.all() + if user_story is not None: + tasks = tasks.filter(user_story=user_story) + if status is not None: + tasks = tasks.filter(status=status) + if milestone is not None: + tasks = tasks.filter(milestone=milestone) + task_orders = {task.id: getattr(task, field) for task in tasks} + new_task_orders = {e["task_id"]: e["order"] for e in bulk_data} + apply_order_updates(task_orders, new_task_orders) + + task_ids = task_orders.keys() events.emit_event_for_ids(ids=task_ids, content_type="tasks.task", projectid=project.pk) - db.update_in_bulk_with_ids(task_ids, new_order_values, model=models.Task) + db.update_attr_in_bulk_for_ids(task_orders, field, models.Task) + return task_orders def snapshot_tasks_in_bulk(bulk_data, user): diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index ddb3f33b..3dd634bd 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -66,4 +66,7 @@ class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator): class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() + milestone_id = serializers.IntegerField(required=False) + status_id = serializers.IntegerField(required=False) + us_id = serializers.IntegerField(required=False) bulk_tasks = _TaskOrderBulkValidator(many=True) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 0e718d10..143cb1ea 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -31,6 +31,7 @@ from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet from taiga.base.api.utils import get_object_or_404 +from taiga.base.utils import json from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.services import take_snapshot @@ -118,6 +119,21 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi raise exc.PermissionDenied(_("You don't have permissions to set this status " "to this user story.")) + """ + Updating some attributes of the userstory can affect the ordering in the backlog, kanban or taskboard + These three methods generate a key for the user story and can be used to be compared before and after + saving + If there is any difference it means an extra ordering update must be done + """ + def _backlog_order_key(self, obj): + return "{}-{}".format(obj.project_id, obj.backlog_order) + + def _kanban_order_key(self, obj): + return "{}-{}-{}".format(obj.project_id, obj.status_id, obj.kanban_order) + + def _sprint_order_key(self, obj): + return "{}-{}-{}".format(obj.project_id, obj.milestone_id, obj.sprint_order) + def pre_save(self, obj): # This is very ugly hack, but having # restframework is the only way to do it. @@ -129,10 +145,55 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi if not obj.id: obj.owner = self.request.user + else: + self._old_backlog_order_key = self._backlog_order_key(self.get_object()) + self._old_kanban_order_key = self._kanban_order_key(self.get_object()) + self._old_sprint_order_key = self._sprint_order_key(self.get_object()) super().pre_save(obj) + def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr, + project, status=None, milestone=None): + # Executes the extra ordering if there is a difference in the ordering keys + if old_order_key != order_key: + extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) + data = [{"us_id": obj.id, "order": getattr(obj, order_attr)}] + for id, order in extra_orders.items(): + data.append({"us_id": int(id), "order": order}) + + return services.update_userstories_order_in_bulk(data, + order_attr, + project, + status=status, + milestone=milestone) + return {} + def post_save(self, obj, created=False): + if not created: + # Let's reorder the related stuff after edit the element + orders_updated = {} + updated = self._reorder_if_needed(obj, + self._old_backlog_order_key, + self._backlog_order_key(obj), + "backlog_order", + obj.project) + orders_updated.update(updated) + updated = self._reorder_if_needed(obj, + self._old_kanban_order_key, + self._kanban_order_key(obj), + "kanban_order", + obj.project, + status=obj.status) + orders_updated.update(updated) + updated = self._reorder_if_needed(obj, + self._old_sprint_order_key, + self._sprint_order_key(obj), + "sprint_order", + obj.project, + milestone=obj.milestone) + orders_updated.update(updated) + self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated) + # Code related to the hack of pre_save method. # Rather, this is the continuation of it. if self._role_points: @@ -180,6 +241,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def update(self, request, *args, **kwargs): self.object = self.get_object_or_none() project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: try: new_project = Project.objects.get(pk=project_id) @@ -295,17 +357,26 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi data = validator.data project = get_object_or_404(Project, pk=data["project_id"]) + status = None + status_id = data.get("status_id", None) + if status_id is not None: + status = get_object_or_404(UserStoryStatus, pk=status_id) + + milestone = None + milestone_id = data.get("milestone_id", None) + if milestone_id is not None: + milestone = get_object_or_404(Milestone, pk=milestone_id) self.check_permissions(request, "bulk_update_order", project) if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) - services.update_userstories_order_in_bulk(data["bulk_stories"], - project=project, - field=order_field) - services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) - - return response.NoContent() + ret = services.update_userstories_order_in_bulk(data["bulk_stories"], + order_field, + project, + status=status, + milestone=milestone) + return response.Ok(ret) @list_route(methods=["POST"]) def bulk_update_backlog_order(self, request, **kwargs): diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 61fe52ec..f1c5d683 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -28,9 +28,9 @@ from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot +from taiga.projects.services import apply_order_updates from taiga.projects.userstories.apps import connect_userstories_signals from taiga.projects.userstories.apps import disconnect_userstories_signals - from taiga.events import events from taiga.projects.votes.utils import attach_total_voters_to_queryset from taiga.projects.notifications.utils import attach_watchers_to_queryset @@ -75,24 +75,32 @@ def create_userstories_in_bulk(bulk_data, callback=None, precall=None, **additio return userstories -def update_userstories_order_in_bulk(bulk_data: list, field: str, project: object): +def update_userstories_order_in_bulk(bulk_data: list, field: str, project: object, + status: object=None, milestone: object=None): """ - Update the order of some user stories. - `bulk_data` should be a list of tuples with the following format: + Updates the order of the userstories specified adding the extra updates needed + to keep consistency. + `bulk_data` should be a list of dicts with the following format: + `field` is the order field used - [(, {: , ...}), ...] + [{'us_id': , 'order': }, ...] """ - user_story_ids = [] - new_order_values = [] - for us_data in bulk_data: - user_story_ids.append(us_data["us_id"]) - new_order_values.append({field: us_data["order"]}) + user_stories = project.user_stories.all() + if status is not None: + user_stories = user_stories.filter(status=status) + if milestone is not None: + user_stories = user_stories.filter(milestone=milestone) + us_orders = {us.id: getattr(us, field) for us in user_stories} + new_us_orders = {e["us_id"]: e["order"] for e in bulk_data} + apply_order_updates(us_orders, new_us_orders) + + user_story_ids = us_orders.keys() events.emit_event_for_ids(ids=user_story_ids, content_type="userstories.userstory", projectid=project.pk) - - db.update_in_bulk_with_ids(user_story_ids, new_order_values, model=models.UserStory) + db.update_attr_in_bulk_for_ids(us_orders, field, models.UserStory) + return us_orders def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object): @@ -100,14 +108,14 @@ def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object): Update the milestone of some user stories. `bulk_data` should be a list of user story ids: """ - user_story_ids = [us_data["us_id"] for us_data in bulk_data] - new_milestone_values = [{"milestone": milestone.id}] * len(user_story_ids) + us_milestones = {e["us_id"]: milestone.id for e in bulk_data} + user_story_ids = us_milestones.keys() events.emit_event_for_ids(ids=user_story_ids, content_type="userstories.userstory", projectid=milestone.project.pk) - db.update_in_bulk_with_ids(user_story_ids, new_milestone_values, model=models.UserStory) + db.update_attr_in_bulk_for_ids(us_milestones, "milestone_id", model=models.UserStory) def snapshot_userstories_in_bulk(bulk_data, user): diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 2d61934f..ba470456 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -64,7 +64,7 @@ class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, v class Meta: model = models.UserStory depth = 0 - read_only_fields = ('created_date', 'modified_date', 'owner') + read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator, @@ -84,6 +84,8 @@ class _UserStoryOrderBulkValidator(UserStoryExistsValidator, validators.Validato class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator, validators.Validator): project_id = serializers.IntegerField() + status_id = serializers.IntegerField(required=False) + milestone_id = serializers.IntegerField(required=False) bulk_stories = _UserStoryOrderBulkValidator(many=True) diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 4eb0c416..bf8c596d 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -659,21 +659,21 @@ def test_user_story_action_bulk_update_order(client, data): "project_id": data.public_project.pk }) results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 204, 204] + assert results == [401, 403, 403, 200, 200] post_data = json.dumps({ "bulk_stories": [{"us_id": data.private_user_story1.id, "order": 2}], "project_id": data.private_project1.pk }) results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 204, 204] + assert results == [401, 403, 403, 200, 200] post_data = json.dumps({ "bulk_stories": [{"us_id": data.private_user_story2.id, "order": 2}], "project_id": data.private_project2.pk }) results = helper_test_http_method(client, 'post', url, post_data, users) - assert results == [401, 403, 403, 204, 204] + assert results == [401, 403, 403, 200, 200] post_data = json.dumps({ "bulk_stories": [{"us_id": data.blocked_user_story.id, "order": 2}], diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 4ea78a35..5a0cc00c 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -56,15 +56,6 @@ Issue #2 db.save_in_bulk.assert_called_once_with(issues, None, None) -def test_update_issues_order_in_bulk(): - data = [(1, 1), (2, 2)] - - with mock.patch("taiga.projects.issues.services.db") as db: - services.update_issues_order_in_bulk(data) - db.update_in_bulk_with_ids.assert_called_once_with([1, 2], [{"order": 1}, {"order": 2}], - model=models.Issue) - - def test_create_issue_without_status(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index c12e1ecb..398a40a2 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -148,8 +148,8 @@ def test_api_update_order_in_bulk(client): response1 = client.json.post(url1, json.dumps(data)) response2 = client.json.post(url2, json.dumps(data)) - assert response1.status_code == 204, response1.data - assert response2.status_code == 204, response2.data + assert response1.status_code == 200, response1.data + assert response2.status_code == 200, response2.data def test_get_invalid_csv(client): diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 10805f8e..1d3984bd 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -50,17 +50,16 @@ def test_create_userstories_in_bulk(): def test_update_userstories_order_in_bulk(): - data = [{"us_id": 1, "order": 1}, {"us_id": 2, "order": 2}] - - project = mock.Mock() - project.pk = 1 + project = f.ProjectFactory.create() + us1 = f.UserStoryFactory.create(project=project, backlog_order=1) + us2 = f.UserStoryFactory.create(project=project, backlog_order=2) + data = [{"us_id": us1.id, "order": 1}, {"us_id": us2.id, "order": 2}] with mock.patch("taiga.projects.userstories.services.db") as db: services.update_userstories_order_in_bulk(data, "backlog_order", project) - db.update_in_bulk_with_ids.assert_called_once_with([1, 2], - [{"backlog_order": 1}, - {"backlog_order": 2}], - model=models.UserStory) + db.update_attr_in_bulk_for_ids.assert_called_once_with({us1.id: 1, us2.id: 2}, + "backlog_order", + models.UserStory) def test_create_userstory_with_watchers(client): @@ -176,9 +175,9 @@ def test_api_update_orders_in_bulk(client): response2 = client.json.post(url2, json.dumps(data)) response3 = client.json.post(url3, json.dumps(data)) - assert response1.status_code == 204, response1.data - assert response2.status_code == 204, response2.data - assert response3.status_code == 204, response3.data + assert response1.status_code == 200, response1.data + assert response2.status_code == 200, response2.data + assert response3.status_code == 200, response3.data def test_api_update_milestone_in_bulk(client): diff --git a/tests/unit/test_order_updates.py b/tests/unit/test_order_updates.py new file mode 100644 index 00000000..f7660bf0 --- /dev/null +++ b/tests/unit/test_order_updates.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.projects.services import apply_order_updates + + +def test_apply_order_updates_one_element_backward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "d": 2 + } + apply_order_updates(orders, new_orders) + assert orders == { + "d": 2, + "b": 3, + "c": 4 + } + + +def test_apply_order_updates_one_element_forward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "a": 3 + } + apply_order_updates(orders, new_orders) + assert orders == { + "a": 3, + "c": 4, + "d": 5, + "e": 6, + "f": 7 + } + + +def test_apply_order_updates_multiple_elements_backward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "d": 2, + "e": 3 + } + apply_order_updates(orders, new_orders) + assert orders == { + "d": 2, + "e": 3, + "b": 4, + "c": 5 + } + +def test_apply_order_updates_multiple_elements_forward(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6 + } + new_orders = { + "a": 4, + "b": 5 + } + apply_order_updates(orders, new_orders) + assert orders == { + "a": 4, + "b": 5, + "d": 6, + "e": 7, + "f": 8 + } + +def test_apply_order_updates_two_elements(): + orders = { + "a": 0, + "b": 1, + } + new_orders = { + "b": 0 + } + apply_order_updates(orders, new_orders) + assert orders == { + "b": 0, + "a": 1 + } + +def test_apply_order_updates_duplicated_orders(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 3, + "e": 3, + "f": 4 + } + new_orders = { + "a": 3 + } + apply_order_updates(orders, new_orders) + print(orders) + assert orders == { + "a": 3, + "c": 4, + "d": 4, + "e": 4, + "f": 5 + } + +def test_apply_order_updates_multiple_elements_duplicated_orders(): + orders = { + "a": 1, + "b": 2, + "c": 3, + "d": 3, + "e": 3, + "f": 4 + } + new_orders = { + "c": 3, + "d": 3, + "a": 4 + } + apply_order_updates(orders, new_orders) + print(orders) + assert orders == { + "c": 3, + "d": 3, + "a": 4, + "e": 5, + "f": 6 + } diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c564b094..2264e970 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -23,7 +23,7 @@ import django_sites as sites import re from taiga.base.utils.urls import get_absolute_url, is_absolute_url, build_url -from taiga.base.utils.db import save_in_bulk, update_in_bulk, update_in_bulk_with_ids, to_tsquery +from taiga.base.utils.db import save_in_bulk, update_in_bulk, to_tsquery pytestmark = pytest.mark.django_db @@ -82,21 +82,6 @@ def test_update_in_bulk_with_a_callback(): assert callback.call_count == 2 -def test_update_in_bulk_with_ids(): - ids = [1, 2] - new_values = [{"field1": 1}, {"field2": 2}] - model = mock.Mock() - - update_in_bulk_with_ids(ids, new_values, model) - - expected_calls = [ - mock.call(id=1), mock.call().update(field1=1), - mock.call(id=2), mock.call().update(field2=2) - ] - - model.objects.filter.assert_has_calls(expected_calls) - - TS_QUERY_TRANSFORMATIONS = [ ("1 OR 2", "1 | 2"), ("(1) 2", "( 1 ) & 2"), From 1b6077c105cb769f6ee2b9f11012f0f1b1cbd442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 7 Jul 2016 09:58:26 +0200 Subject: [PATCH 123/261] Migrating export_import api to new serializers/validators --- taiga/base/fields.py | 22 +- taiga/export_import/api.py | 59 +- taiga/export_import/serializers/fields.py | 194 +------ taiga/export_import/serializers/mixins.py | 111 ++-- .../export_import/serializers/serializers.py | 512 +++++++++--------- taiga/export_import/services/render.py | 67 +-- taiga/export_import/services/store.py | 383 ++++++------- taiga/export_import/validators/__init__.py | 27 + taiga/export_import/validators/cache.py | 42 ++ taiga/export_import/validators/fields.py | 196 +++++++ taiga/export_import/validators/mixins.py | 97 ++++ taiga/export_import/validators/validators.py | 349 ++++++++++++ 12 files changed, 1294 insertions(+), 765 deletions(-) create mode 100644 taiga/export_import/validators/__init__.py create mode 100644 taiga/export_import/validators/cache.py create mode 100644 taiga/export_import/validators/fields.py create mode 100644 taiga/export_import/validators/mixins.py create mode 100644 taiga/export_import/validators/validators.py diff --git a/taiga/base/fields.py b/taiga/base/fields.py index 30be6b60..3b19f15f 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -18,7 +18,8 @@ from django.forms import widgets from django.utils.translation import ugettext as _ -from taiga.base.api import serializers +from taiga.base.api import serializers, ISO_8601 +from taiga.base.api.settings import api_settings import serpy @@ -128,4 +129,21 @@ class I18NJsonField(Field): class FileField(Field): def to_value(self, value): - return value.name + if value: + return value.name + return None + + +class DateTimeField(Field): + format = api_settings.DATETIME_FORMAT + + def to_value(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) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index da2af132..75644365 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -44,11 +44,11 @@ from taiga.users import services as users_services from . import exceptions as err from . import mixins from . import permissions +from . import validators from . import serializers from . import services from . import tasks from . import throttling -from .renderers import ExportRenderer from taiga.base.api.utils import get_object_or_404 @@ -102,9 +102,8 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Validate if the project can be imported is_private = data.get('is_private', False) - total_memberships = len([m for m in data.get("memberships", []) - if m.get("email", None) != data["owner"]]) - total_memberships = total_memberships + 1 # 1 is the owner + total_memberships = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]]) + total_memberships = total_memberships + 1 # 1 is the owner (enough_slots, error_message) = users_services.has_available_slot_for_import_new_project( self.request.user, is_private, @@ -147,31 +146,31 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Create project values choicess if "points" in data: services.store.store_project_attributes_values(project_serialized.object, data, - "points", serializers.PointsExportSerializer) + "points", validators.PointsExportValidator) if "issue_types" in data: services.store.store_project_attributes_values(project_serialized.object, data, "issue_types", - serializers.IssueTypeExportSerializer) + validators.IssueTypeExportValidator) if "issue_statuses" in data: services.store.store_project_attributes_values(project_serialized.object, data, "issue_statuses", - serializers.IssueStatusExportSerializer,) + validators.IssueStatusExportValidator,) if "us_statuses" in data: services.store.store_project_attributes_values(project_serialized.object, data, "us_statuses", - serializers.UserStoryStatusExportSerializer,) + validators.UserStoryStatusExportValidator,) if "task_statuses" in data: services.store.store_project_attributes_values(project_serialized.object, data, "task_statuses", - serializers.TaskStatusExportSerializer) + validators.TaskStatusExportValidator) if "priorities" in data: services.store.store_project_attributes_values(project_serialized.object, data, "priorities", - serializers.PriorityExportSerializer) + validators.PriorityExportValidator) if "severities" in data: services.store.store_project_attributes_values(project_serialized.object, data, "severities", - serializers.SeverityExportSerializer) + validators.SeverityExportValidator) if ("points" in data or "issues_types" in data or "issues_statuses" in data or "us_statuses" in data or @@ -183,17 +182,17 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if "userstorycustomattributes" in data: services.store.store_custom_attributes(project_serialized.object, data, "userstorycustomattributes", - serializers.UserStoryCustomAttributeExportSerializer) + validators.UserStoryCustomAttributeExportValidator) if "taskcustomattributes" in data: services.store.store_custom_attributes(project_serialized.object, data, "taskcustomattributes", - serializers.TaskCustomAttributeExportSerializer) + validators.TaskCustomAttributeExportValidator) if "issuecustomattributes" in data: services.store.store_custom_attributes(project_serialized.object, data, "issuecustomattributes", - serializers.IssueCustomAttributeExportSerializer) + validators.IssueCustomAttributeExportValidator) # Is there any error? errors = services.store.get_errors() @@ -201,7 +200,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi raise exc.BadRequest(errors) # Importer process is OK - response_data = project_serialized.data + response_data = serializers.ProjectExportSerializer(project_serialized.object).data response_data['id'] = project_serialized.object.id headers = self.get_success_headers(response_data) return response.Created(response_data, headers=headers) @@ -218,8 +217,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(milestone.data) - return response.Created(milestone.data, headers=headers) + data = serializers.MilestoneExportSerializer(milestone.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -233,8 +233,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(us.data) - return response.Created(us.data, headers=headers) + data = serializers.UserStoryExportSerializer(us.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -251,8 +252,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(task.data) - return response.Created(task.data, headers=headers) + data = serializers.TaskExportSerializer(task.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -269,8 +271,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(issue.data) - return response.Created(issue.data, headers=headers) + data = serializers.IssueExportSerializer(issue.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -284,8 +287,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(wiki_page.data) - return response.Created(wiki_page.data, headers=headers) + data = serializers.WikiPageExportSerializer(wiki_page.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @detail_route(methods=['post']) @method_decorator(atomic) @@ -299,8 +303,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi if errors: raise exc.BadRequest(errors) - headers = self.get_success_headers(wiki_link.data) - return response.Created(wiki_link.data, headers=headers) + data = serializers.WikiLinkExportSerializer(wiki_link.object).data + headers = self.get_success_headers(data) + return response.Created(data, headers=headers) @list_route(methods=["POST"]) @method_decorator(atomic) diff --git a/taiga/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py index 9ed21a19..29ec85aa 100644 --- a/taiga/export_import/serializers/fields.py +++ b/taiga/export_import/serializers/fields.py @@ -21,24 +21,15 @@ import os import copy from collections import OrderedDict -from django.core.files.base import ContentFile -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import ugettext as _ -from django.contrib.contenttypes.models import ContentType - from taiga.base.api import serializers -from taiga.base.exceptions import ValidationError -from taiga.base.fields import JsonField -from taiga.mdrender.service import render as mdrender +from taiga.base.fields import Field from taiga.users import models as users_models -from .cache import cached_get_user_by_email, cached_get_user_by_pk +from .cache import cached_get_user_by_pk -class FileField(serializers.WritableField): - read_only = False - - def to_native(self, obj): +class FileField(Field): + def to_value(self, obj): if not obj: return None @@ -49,202 +40,74 @@ class FileField(serializers.WritableField): ("name", os.path.basename(obj.name)), ]) - def from_native(self, data): - if not data: - return None - decoded_data = b'' - # The original file was encoded by chunks but we don't really know its - # length or if it was multiple of 3 so we must iterate over all those chunks - # decoding them one by one - for decoding_chunk in data['data'].split("="): - # When encoding to base64 3 bytes are transformed into 4 bytes and - # the extra space of the block is filled with = - # We must ensure that the decoding chunk has a length multiple of 4 so - # we restore the stripped '='s adding appending them until the chunk has - # a length multiple of 4 - decoding_chunk += "=" * (-len(decoding_chunk) % 4) - decoded_data += base64.b64decode(decoding_chunk+"=") - - return ContentFile(decoded_data, name=data['name']) - - -class ContentTypeField(serializers.RelatedField): - read_only = False - - def to_native(self, obj): +class ContentTypeField(Field): + def to_value(self, obj): if obj: return [obj.app_label, obj.model] return None - def from_native(self, data): - try: - return ContentType.objects.get_by_natural_key(*data) - except Exception: - return None - -class RelatedNoneSafeField(serializers.RelatedField): - 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() - - key = self.source or field_name - if value in self.null_values: - if self.required: - raise ValidationError(self.error_messages['required']) - into[key] = None - elif self.many: - into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] - else: - into[key] = self.from_native(value) - - -class UserRelatedField(RelatedNoneSafeField): - read_only = False - - def to_native(self, obj): +class UserRelatedField(Field): + def to_value(self, obj): if obj: return obj.email return None - def from_native(self, data): - try: - return cached_get_user_by_email(data) - except users_models.User.DoesNotExist: - return None - -class UserPkField(serializers.RelatedField): - read_only = False - - def to_native(self, obj): +class UserPkField(Field): + def to_value(self, obj): try: user = cached_get_user_by_pk(obj) return user.email except users_models.User.DoesNotExist: return None - def from_native(self, data): - try: - user = cached_get_user_by_email(data) - return user.pk - except users_models.User.DoesNotExist: - return None - - -class CommentField(serializers.WritableField): - read_only = False - - def field_from_native(self, data, files, field_name, into): - super().field_from_native(data, files, field_name, into) - into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) - - -class ProjectRelatedField(serializers.RelatedField): - read_only = False - null_values = (None, "") +class SlugRelatedField(Field): def __init__(self, slug_field, *args, **kwargs): self.slug_field = slug_field super().__init__(*args, **kwargs) - def to_native(self, obj): + def to_value(self, obj): if obj: return getattr(obj, self.slug_field) return None - def from_native(self, data): - try: - 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))) - -class HistoryUserField(JsonField): - def to_native(self, obj): +class HistoryUserField(Field): + def to_value(self, obj): if obj is None or obj == {}: return [] try: user = cached_get_user_by_pk(obj['pk']) except users_models.User.DoesNotExist: user = None - return (UserRelatedField().to_native(user), obj['name']) - - def from_native(self, data): - if data is None: - return {} - - if len(data) < 2: - return {} - - user = UserRelatedField().from_native(data[0]) - - if user: - pk = user.pk - else: - pk = None - - return {"pk": pk, "name": data[1]} + return (UserRelatedField().to_value(user), obj['name']) -class HistoryValuesField(JsonField): - def to_native(self, obj): +class HistoryValuesField(Field): + def to_value(self, obj): if obj is None: return [] if "users" in obj: - obj['users'] = list(map(UserPkField().to_native, obj['users'])) + obj['users'] = list(map(UserPkField().to_value, obj['users'])) return obj - def from_native(self, data): - if data is None: - return [] - if "users" in data: - data['users'] = list(map(UserPkField().from_native, data['users'])) - return data - -class HistoryDiffField(JsonField): - def to_native(self, obj): +class HistoryDiffField(Field): + def to_value(self, obj): if obj is None: return [] if "assigned_to" in obj: - obj['assigned_to'] = list(map(UserPkField().to_native, obj['assigned_to'])) + obj['assigned_to'] = list(map(UserPkField().to_value, obj['assigned_to'])) return obj - def from_native(self, data): - if data is None: - return [] - if "assigned_to" in data: - data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) - return data - - -class TimelineDataField(serializers.WritableField): - read_only = False - - def to_native(self, data): +class TimelineDataField(Field): + def to_value(self, data): new_data = copy.deepcopy(data) try: user = cached_get_user_by_pk(new_data["user"]["id"]) @@ -253,14 +116,3 @@ class TimelineDataField(serializers.WritableField): except Exception: pass return new_data - - def from_native(self, data): - new_data = copy.deepcopy(data) - try: - user = cached_get_user_by_email(new_data["user"]["email"]) - new_data["user"]["id"] = user.id - del new_data["user"]["email"] - except users_models.User.DoesNotExist: - pass - - return new_data diff --git a/taiga/export_import/serializers/mixins.py b/taiga/export_import/serializers/mixins.py index 007649a2..3006500f 100644 --- a/taiga/export_import/serializers/mixins.py +++ b/taiga/export_import/serializers/mixins.py @@ -16,56 +16,62 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField, DateTimeField from taiga.projects.history import models as history_models from taiga.projects.attachments import models as attachments_models -from taiga.projects.notifications import services as notifications_services from taiga.projects.history import services as history_service from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField, - JsonField, HistoryValuesField, CommentField, FileField) + HistoryValuesField, FileField) -class HistoryExportSerializer(serializers.ModelSerializer): +class HistoryExportSerializer(serializers.LightSerializer): user = HistoryUserField() - diff = HistoryDiffField(required=False) - snapshot = JsonField(required=False) - values = HistoryValuesField(required=False) - comment = CommentField(required=False) - delete_comment_date = serializers.DateTimeField(required=False) - delete_comment_user = HistoryUserField(required=False) - - class Meta: - model = history_models.HistoryEntry - exclude = ("id", "comment_html", "key") + diff = HistoryDiffField() + snapshot = Field() + values = HistoryValuesField() + comment = Field() + delete_comment_date = DateTimeField() + delete_comment_user = HistoryUserField() + comment_versions = Field() + created_at = DateTimeField() + edit_comment_date = DateTimeField() + is_hidden = Field() + is_snapshot = Field() + type = Field() -class HistoryExportSerializerMixin(serializers.ModelSerializer): - history = serializers.SerializerMethodField("get_history") +class HistoryExportSerializerMixin(serializers.LightSerializer): + history = MethodField("get_history") def get_history(self, obj): - history_qs = history_service.get_history_queryset_by_model_instance(obj, - types=(history_models.HistoryType.change, history_models.HistoryType.create,)) + history_qs = history_service.get_history_queryset_by_model_instance( + obj, + types=(history_models.HistoryType.change, history_models.HistoryType.create,) + ) return HistoryExportSerializer(history_qs, many=True).data -class AttachmentExportSerializer(serializers.ModelSerializer): - owner = UserRelatedField(required=False) +class AttachmentExportSerializer(serializers.LightSerializer): + owner = UserRelatedField() attached_file = FileField() - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = attachments_models.Attachment - exclude = ('id', 'content_type', 'object_id', 'project') + created_date = DateTimeField() + modified_date = DateTimeField() + description = Field() + is_deprecated = Field() + name = Field() + order = Field() + sha1 = Field() + size = Field() -class AttachmentExportSerializerMixin(serializers.ModelSerializer): - attachments = serializers.SerializerMethodField("get_attachments") +class AttachmentExportSerializerMixin(serializers.LightSerializer): + attachments = MethodField() def get_attachments(self, obj): content_type = ContentType.objects.get_for_model(obj.__class__) @@ -74,8 +80,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer): return AttachmentExportSerializer(attachments_qs, many=True).data -class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): - custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") +class CustomAttributesValuesExportSerializerMixin(serializers.LightSerializer): + custom_attributes_values = MethodField("get_custom_attributes_values") def custom_attributes_queryset(self, project): raise NotImplementedError() @@ -85,13 +91,13 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): ret = {} for attr in custom_attributes: value = values.get(str(attr["id"]), None) - if value is not None: + if value is not None: ret[attr["name"]] = value return ret try: - values = obj.custom_attributes_values.attributes_values + values = obj.custom_attributes_values.attributes_values custom_attributes = self.custom_attributes_queryset(obj.project) return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) @@ -99,43 +105,8 @@ class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer): return None -class WatcheableObjectModelSerializerMixin(serializers.ModelSerializer): - watchers = UserRelatedField(many=True, required=False) +class WatcheableObjectLightSerializerMixin(serializers.LightSerializer): + watchers = MethodField() - def __init__(self, *args, **kwargs): - self._watchers_field = self.base_fields.pop("watchers", None) - super(WatcheableObjectModelSerializerMixin, self).__init__(*args, **kwargs) - - """ - watchers is not a field from the model so we need to do some magic to make it work like a normal field - It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances - """ - - def restore_object(self, attrs, instance=None): - watcher_field = self.fields.pop("watchers", None) - instance = super(WatcheableObjectModelSerializerMixin, self).restore_object(attrs, instance) - self._watchers = self.init_data.get("watchers", []) - return instance - - def save_watchers(self): - new_watcher_emails = set(self._watchers) - old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) - adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) - removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) - - User = get_user_model() - adding_users = User.objects.filter(email__in=adding_watcher_emails) - removing_users = User.objects.filter(email__in=removing_watcher_emails) - - for user in adding_users: - notifications_services.add_watcher(self.object, user) - - for user in removing_users: - notifications_services.remove_watcher(self.object, user) - - self.object.watchers = [user.email for user in self.object.get_watchers()] - - def to_native(self, obj): - ret = super(WatcheableObjectModelSerializerMixin, self).to_native(obj) - ret["watchers"] = [user.email for user in obj.get_watchers()] - return ret + def get_watchers(self, obj): + return [user.email for user in obj.get_watchers()] diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py index 6a316b68..ff7e791c 100644 --- a/taiga/export_import/serializers/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -16,231 +16,183 @@ # 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.base.api import serializers -from taiga.base.fields import JsonField, PgArrayField -from taiga.base.exceptions import ValidationError +from taiga.base.fields import Field, DateTimeField, MethodField -from taiga.projects import models as projects_models -from taiga.projects.custom_attributes import models as custom_attributes_models -from taiga.projects.userstories import models as userstories_models -from taiga.projects.tasks import models as tasks_models -from taiga.projects.issues import models as issues_models -from taiga.projects.milestones import models as milestones_models -from taiga.projects.wiki import models as wiki_models -from taiga.timeline import models as timeline_models -from taiga.users import models as users_models from taiga.projects.votes import services as votes_service -from .fields import (FileField, UserRelatedField, - ProjectRelatedField, - TimelineDataField, ContentTypeField) +from .fields import (FileField, UserRelatedField, TimelineDataField, + ContentTypeField, SlugRelatedField) from .mixins import (HistoryExportSerializerMixin, AttachmentExportSerializerMixin, CustomAttributesValuesExportSerializerMixin, - WatcheableObjectModelSerializerMixin) + WatcheableObjectLightSerializerMixin) from .cache import (_custom_tasks_attributes_cache, _custom_userstories_attributes_cache, _custom_issues_attributes_cache) -class PointsExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.Points - exclude = ('id', 'project') +class RelatedExportSerializer(serializers.LightSerializer): + def to_value(self, value): + if hasattr(value, 'all'): + return super().to_value(value.all()) + return super().to_value(value) -class UserStoryStatusExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.UserStoryStatus - exclude = ('id', 'project') +class PointsExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + value = Field() -class TaskStatusExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.TaskStatus - exclude = ('id', 'project') +class UserStoryStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + is_archived = Field() + color = Field() + wip_limit = Field() -class IssueStatusExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.IssueStatus - exclude = ('id', 'project') +class TaskStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + color = Field() -class PriorityExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.Priority - exclude = ('id', 'project') +class IssueStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + color = Field() -class SeverityExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.Severity - exclude = ('id', 'project') +class PriorityExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + color = Field() -class IssueTypeExportSerializer(serializers.ModelSerializer): - class Meta: - model = projects_models.IssueType - exclude = ('id', 'project') +class SeverityExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + color = Field() -class RoleExportSerializer(serializers.ModelSerializer): - permissions = PgArrayField(required=False) - - class Meta: - model = users_models.Role - exclude = ('id', 'project') +class IssueTypeExportSerializer(RelatedExportSerializer): + name = Field() + order = Field() + color = Field() -class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer): - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = custom_attributes_models.UserStoryCustomAttribute - exclude = ('id', 'project') +class RoleExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + computable = Field() + permissions = Field() -class TaskCustomAttributeExportSerializer(serializers.ModelSerializer): - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = custom_attributes_models.TaskCustomAttribute - exclude = ('id', 'project') +class UserStoryCustomAttributeExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() -class IssueCustomAttributeExportSerializer(serializers.ModelSerializer): - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = custom_attributes_models.IssueCustomAttribute - exclude = ('id', 'project') +class TaskCustomAttributeExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() -class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): - attributes_values = JsonField(source="attributes_values", required=True) - _custom_attribute_model = None - _container_field = None +class IssueCustomAttributeExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() - class Meta: - exclude = ("id",) - def validate_attributes_values(self, attrs, source): - # values must be a dict - data_values = attrs.get("attributes_values", None) - if self.object: - data_values = (data_values or self.object.attributes_values) - - if type(data_values) is not dict: - raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) - - # Values keys must be in the container object project - data_container = attrs.get(self._container_field, None) - if data_container: - project_id = data_container.project_id - elif self.object: - project_id = getattr(self.object, self._container_field).project_id - else: - project_id = None - - values_ids = list(data_values.keys()) - qs = self._custom_attribute_model.objects.filter(project=project_id, - id__in=values_ids) - if qs.count() != len(values_ids): - raise ValidationError(_("It contain invalid custom fields.")) - - return attrs +class BaseCustomAttributesValuesExportSerializer(RelatedExportSerializer): + attributes_values = Field(required=True) class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): - _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute - _container_model = "userstories.UserStory" - _container_field = "user_story" - - class Meta(BaseCustomAttributesValuesExportSerializer.Meta): - model = custom_attributes_models.UserStoryCustomAttributesValues + user_story = Field(attr="user_story.id") class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): - _custom_attribute_model = custom_attributes_models.TaskCustomAttribute - _container_field = "task" - - class Meta(BaseCustomAttributesValuesExportSerializer.Meta): - model = custom_attributes_models.TaskCustomAttributesValues + task = Field(attr="task.id") class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): - _custom_attribute_model = custom_attributes_models.IssueCustomAttribute - _container_field = "issue" - - class Meta(BaseCustomAttributesValuesExportSerializer.Meta): - model = custom_attributes_models.IssueCustomAttributesValues + issue = Field(attr="issue.id") -class MembershipExportSerializer(serializers.ModelSerializer): - user = UserRelatedField(required=False) - role = ProjectRelatedField(slug_field="name") - invited_by = UserRelatedField(required=False) - - class Meta: - model = projects_models.Membership - exclude = ('id', 'project', 'token') - - def full_clean(self, instance): - return instance +class MembershipExportSerializer(RelatedExportSerializer): + user = UserRelatedField() + role = SlugRelatedField(slug_field="name") + invited_by = UserRelatedField() + is_admin = Field() + email = Field() + created_at = DateTimeField() + invitation_extra_text = Field() + user_order = Field() -class RolePointsExportSerializer(serializers.ModelSerializer): - role = ProjectRelatedField(slug_field="name") - points = ProjectRelatedField(slug_field="name") - - class Meta: - model = userstories_models.RolePoints - exclude = ('id', 'user_story') +class RolePointsExportSerializer(RelatedExportSerializer): + role = SlugRelatedField(slug_field="name") + points = SlugRelatedField(slug_field="name") -class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - modified_date = serializers.DateTimeField(required=False) - estimated_start = serializers.DateField(required=False) - estimated_finish = serializers.DateField(required=False) - - def __init__(self, *args, **kwargs): - project = kwargs.pop('project', None) - super(MilestoneExportSerializer, self).__init__(*args, **kwargs) - if project: - self.project = project - - def validate_name(self, attrs, source): - """ - Check the milestone name is not duplicated in the project - """ - name = attrs[source] - qs = self.project.milestones.filter(name=name) - if qs.exists(): - raise ValidationError(_("Name duplicated for the project")) - - return attrs - - class Meta: - model = milestones_models.Milestone - exclude = ('id', 'project') +class MilestoneExportSerializer(WatcheableObjectLightSerializerMixin, RelatedExportSerializer): + name = Field() + owner = UserRelatedField() + created_date = DateTimeField() + modified_date = DateTimeField() + estimated_start = Field() + estimated_finish = Field() + slug = Field() + closed = Field() + disponibility = Field() + order = Field() -class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - status = ProjectRelatedField(slug_field="name") - user_story = ProjectRelatedField(slug_field="ref", required=False) - milestone = ProjectRelatedField(slug_field="name", required=False) - assigned_to = UserRelatedField(required=False) - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = tasks_models.Task - exclude = ('id', 'project') +class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + owner = UserRelatedField() + status = SlugRelatedField(slug_field="name") + user_story = SlugRelatedField(slug_field="ref") + milestone = SlugRelatedField(slug_field="name") + assigned_to = UserRelatedField() + modified_date = DateTimeField() + created_date = DateTimeField() + finished_date = DateTimeField() + ref = Field() + subject = Field() + us_order = Field() + taskboard_order = Field() + description = Field() + is_iocaine = Field() + external_reference = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() def custom_attributes_queryset(self, project): if project.id not in _custom_tasks_attributes_cache: @@ -248,19 +200,35 @@ class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryE return _custom_tasks_attributes_cache[project.id] -class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): - role_points = RolePointsExportSerializer(many=True, required=False) - owner = UserRelatedField(required=False) - assigned_to = UserRelatedField(required=False) - status = ProjectRelatedField(slug_field="name") - milestone = ProjectRelatedField(slug_field="name", required=False) - modified_date = serializers.DateTimeField(required=False) - generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) - - class Meta: - model = userstories_models.UserStory - exclude = ('id', 'project', 'points', 'tasks') +class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + role_points = RolePointsExportSerializer(many=True) + owner = UserRelatedField() + assigned_to = UserRelatedField() + status = SlugRelatedField(slug_field="name") + milestone = SlugRelatedField(slug_field="name") + modified_date = DateTimeField() + created_date = DateTimeField() + finish_date = DateTimeField() + generated_from_issue = SlugRelatedField(slug_field="ref") + ref = Field() + is_closed = Field() + backlog_order = Field() + sprint_order = Field() + kanban_order = Field() + subject = Field() + description = Field() + client_requirement = Field() + team_requirement = Field() + external_reference = Field() + tribe_gig = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() def custom_attributes_queryset(self, project): if project.id not in _custom_userstories_attributes_cache: @@ -270,21 +238,31 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His return _custom_userstories_attributes_cache[project.id] -class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, - AttachmentExportSerializerMixin, WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - status = ProjectRelatedField(slug_field="name") - assigned_to = UserRelatedField(required=False) - priority = ProjectRelatedField(slug_field="name") - severity = ProjectRelatedField(slug_field="name") - type = ProjectRelatedField(slug_field="name") - milestone = ProjectRelatedField(slug_field="name", required=False) - votes = serializers.SerializerMethodField("get_votes") - modified_date = serializers.DateTimeField(required=False) +class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + owner = UserRelatedField() + status = SlugRelatedField(slug_field="name") + assigned_to = UserRelatedField() + priority = SlugRelatedField(slug_field="name") + severity = SlugRelatedField(slug_field="name") + type = SlugRelatedField(slug_field="name") + milestone = SlugRelatedField(slug_field="name") + votes = MethodField("get_votes") + modified_date = DateTimeField() + created_date = DateTimeField() + finished_date = DateTimeField() - class Meta: - model = issues_models.Issue - exclude = ('id', 'project') + ref = Field() + subject = Field() + description = Field() + external_reference = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() def get_votes(self, obj): return [x.email for x in votes_service.get_voters(obj)] @@ -295,65 +273,93 @@ class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, History return _custom_issues_attributes_cache[project.id] -class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, - WatcheableObjectModelSerializerMixin): - owner = UserRelatedField(required=False) - last_modifier = UserRelatedField(required=False) - modified_date = serializers.DateTimeField(required=False) - - class Meta: - model = wiki_models.WikiPage - exclude = ('id', 'project') +class WikiPageExportSerializer(HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + slug = Field() + owner = UserRelatedField() + last_modifier = UserRelatedField() + modified_date = DateTimeField() + created_date = DateTimeField() + content = Field() + version = Field() -class WikiLinkExportSerializer(serializers.ModelSerializer): - class Meta: - model = wiki_models.WikiLink - exclude = ('id', 'project') +class WikiLinkExportSerializer(RelatedExportSerializer): + title = Field() + href = Field() + order = Field() -class TimelineExportSerializer(serializers.ModelSerializer): +class TimelineExportSerializer(RelatedExportSerializer): data = TimelineDataField() data_content_type = ContentTypeField() - - class Meta: - model = timeline_models.Timeline - exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') + event_type = Field() + created = DateTimeField() -class ProjectExportSerializer(WatcheableObjectModelSerializerMixin): - logo = FileField(required=False) - anon_permissions = PgArrayField(required=False) - public_permissions = PgArrayField(required=False) - modified_date = serializers.DateTimeField(required=False) - roles = RoleExportSerializer(many=True, required=False) - owner = UserRelatedField(required=False) - memberships = MembershipExportSerializer(many=True, required=False) - points = PointsExportSerializer(many=True, required=False) - us_statuses = UserStoryStatusExportSerializer(many=True, required=False) - task_statuses = TaskStatusExportSerializer(many=True, required=False) - issue_types = IssueTypeExportSerializer(many=True, required=False) - issue_statuses = IssueStatusExportSerializer(many=True, required=False) - priorities = PriorityExportSerializer(many=True, required=False) - severities = SeverityExportSerializer(many=True, required=False) - tags_colors = JsonField(required=False) - default_points = serializers.SlugRelatedField(slug_field="name", required=False) - default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) - default_task_status = serializers.SlugRelatedField(slug_field="name", required=False) - default_priority = serializers.SlugRelatedField(slug_field="name", required=False) - default_severity = serializers.SlugRelatedField(slug_field="name", required=False) - default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False) - default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False) - userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False) - taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False) - issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False) - user_stories = UserStoryExportSerializer(many=True, required=False) - tasks = TaskExportSerializer(many=True, required=False) - milestones = MilestoneExportSerializer(many=True, required=False) - issues = IssueExportSerializer(many=True, required=False) - wiki_links = WikiLinkExportSerializer(many=True, required=False) - wiki_pages = WikiPageExportSerializer(many=True, required=False) - - class Meta: - model = projects_models.Project - exclude = ('id', 'creation_template', 'members') +class ProjectExportSerializer(WatcheableObjectLightSerializerMixin): + name = Field() + slug = Field() + description = Field() + created_date = DateTimeField() + logo = FileField() + total_milestones = Field() + total_story_points = Field() + is_backlog_activated = Field() + is_kanban_activated = Field() + is_wiki_activated = Field() + is_issues_activated = Field() + videoconferences = Field() + videoconferences_extra_data = Field() + creation_template = SlugRelatedField(slug_field="slug") + is_private = Field() + is_featured = Field() + is_looking_for_people = Field() + looking_for_people_note = Field() + userstories_csv_uuid = Field() + tasks_csv_uuid = Field() + issues_csv_uuid = Field() + transfer_token = Field() + blocked_code = Field() + totals_updated_datetime = DateTimeField() + total_fans = Field() + total_fans_last_week = Field() + total_fans_last_month = Field() + total_fans_last_year = Field() + total_activity = Field() + total_activity_last_week = Field() + total_activity_last_month = Field() + total_activity_last_year = Field() + anon_permissions = Field() + public_permissions = Field() + modified_date = DateTimeField() + roles = RoleExportSerializer(many=True) + owner = UserRelatedField() + memberships = MembershipExportSerializer(many=True) + points = PointsExportSerializer(many=True) + us_statuses = UserStoryStatusExportSerializer(many=True) + task_statuses = TaskStatusExportSerializer(many=True) + issue_types = IssueTypeExportSerializer(many=True) + issue_statuses = IssueStatusExportSerializer(many=True) + priorities = PriorityExportSerializer(many=True) + severities = SeverityExportSerializer(many=True) + tags_colors = Field() + default_points = SlugRelatedField(slug_field="name") + default_us_status = SlugRelatedField(slug_field="name") + default_task_status = SlugRelatedField(slug_field="name") + default_priority = SlugRelatedField(slug_field="name") + default_severity = SlugRelatedField(slug_field="name") + default_issue_status = SlugRelatedField(slug_field="name") + default_issue_type = SlugRelatedField(slug_field="name") + userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True) + taskcustomattributes = TaskCustomAttributeExportSerializer(many=True) + issuecustomattributes = IssueCustomAttributeExportSerializer(many=True) + user_stories = UserStoryExportSerializer(many=True) + tasks = TaskExportSerializer(many=True) + milestones = MilestoneExportSerializer(many=True) + issues = IssueExportSerializer(many=True) + wiki_links = WikiLinkExportSerializer(many=True) + wiki_pages = WikiPageExportSerializer(many=True) + tags = Field() diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py index 923647a7..0b56f3f5 100644 --- a/taiga/export_import/services/render.py +++ b/taiga/export_import/services/render.py @@ -19,49 +19,44 @@ # This makes all code that import services works and # is not the baddest practice ;) -import base64 import gc -import os - -from django.core.files.storage import default_storage from taiga.base.utils import json +from taiga.base.fields import MethodField from taiga.timeline.service import get_project_timeline from taiga.base.api.fields import get_component from .. import serializers -def render_project(project, outfile, chunk_size = 8190): +def render_project(project, outfile, chunk_size=8190): serializer = serializers.ProjectExportSerializer(project) outfile.write(b'{\n') first_field = True - for field_name in serializer.fields.keys(): + for field_name in serializer._field_map.keys(): # Avoid writing "," in the last element if not first_field: outfile.write(b",\n") else: first_field = False - field = serializer.fields.get(field_name) - field.initialize(parent=serializer, field_name=field_name) + field = serializer._field_map.get(field_name) + # field.initialize(parent=serializer, field_name=field_name) # These four "special" fields hava attachments so we use them in a special way if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]: value = get_component(project, field_name) if field_name != "wiki_pages": - value = value.select_related('owner', 'status', 'milestone', 'project', 'assigned_to', 'custom_attributes_values') + value = value.select_related('owner', 'status', 'milestone', + 'project', 'assigned_to', + 'custom_attributes_values') if field_name == "issues": value = value.select_related('severity', 'priority', 'type') value = value.prefetch_related('history_entry', 'attachments') outfile.write('"{}": [\n'.format(field_name).encode()) - attachments_field = field.fields.pop("attachments", None) - if attachments_field: - attachments_field.initialize(parent=field, field_name="attachments") - first_item = True for item in value.iterator(): # Avoid writing "," in the last element @@ -70,47 +65,18 @@ def render_project(project, outfile, chunk_size = 8190): else: first_item = False - - dumped_value = json.dumps(field.to_native(item)) - writing_value = dumped_value[:-1]+ ',\n "attachments": [\n' - outfile.write(writing_value.encode()) - - first_attachment = True - for attachment in item.attachments.iterator(): - # Avoid writing "," in the last element - if not first_attachment: - outfile.write(b",\n") - else: - first_attachment = False - - # Write all the data expect the serialized file - attachment_serializer = serializers.AttachmentExportSerializer(instance=attachment) - attached_file_serializer = attachment_serializer.fields.pop("attached_file") - dumped_value = json.dumps(attachment_serializer.data) - dumped_value = dumped_value[:-1] + ',\n "attached_file":{\n "data":"' - outfile.write(dumped_value.encode()) - - # We write the attached_files by chunks so the memory used is not increased - attachment_file = attachment.attached_file - if default_storage.exists(attachment_file.name): - with default_storage.open(attachment_file.name) as f: - while True: - bin_data = f.read(chunk_size) - if not bin_data: - break - - b64_data = base64.b64encode(bin_data) - outfile.write(b64_data) - - outfile.write('", \n "name":"{}"}}\n}}'.format( - os.path.basename(attachment_file.name)).encode()) - - outfile.write(b']}') + field.many = False + dumped_value = json.dumps(field.to_value(item)) + outfile.write(dumped_value.encode()) outfile.flush() gc.collect() outfile.write(b']') else: - value = field.field_to_native(project, field_name) + if isinstance(field, MethodField): + value = field.as_getter(field_name, serializers.ProjectExportSerializer)(serializer, project) + else: + attr = getattr(project, field_name) + value = field.to_value(attr) outfile.write('"{}": {}'.format(field_name, json.dumps(value)).encode()) # Generate the timeline @@ -127,4 +93,3 @@ def render_project(project, outfile, chunk_size = 8190): outfile.write(dumped_value.encode()) outfile.write(b']}\n') - diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index 5d71c445..9739bb1e 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -39,7 +39,7 @@ from taiga.timeline.service import build_project_namespace from taiga.users import services as users_service from .. import exceptions as err -from .. import serializers +from .. import validators ######################################################################## @@ -90,13 +90,13 @@ def store_project(data): if key not in excluded_fields: project_data[key] = value - serialized = serializers.ProjectExportSerializer(data=project_data) - if serialized.is_valid(): - serialized.object._importing = True - serialized.object.save() - serialized.save_watchers() - return serialized - add_errors("project", serialized.errors) + validator = validators.ProjectExportValidator(data=project_data) + if validator.is_valid(): + validator.object._importing = True + validator.object.save() + validator.save_watchers() + return validator + add_errors("project", validator.errors) return None @@ -133,54 +133,55 @@ def _store_custom_attributes_values(obj, data_values, obj_field, serializer_clas def _store_attachment(project, obj, attachment): - serialized = serializers.AttachmentExportSerializer(data=attachment) - if serialized.is_valid(): - serialized.object.content_type = ContentType.objects.get_for_model(obj.__class__) - serialized.object.object_id = obj.id - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object.size = serialized.object.attached_file.size - serialized.object.name = os.path.basename(serialized.object.attached_file.name) - serialized.save() - return serialized - add_errors("attachments", serialized.errors) - return serialized + validator = validators.AttachmentExportValidator(data=attachment) + if validator.is_valid(): + validator.object.content_type = ContentType.objects.get_for_model(obj.__class__) + validator.object.object_id = obj.id + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object.size = validator.object.attached_file.size + validator.object.name = os.path.basename(validator.object.attached_file.name) + validator.save() + return validator + add_errors("attachments", validator.errors) + return validator def _store_history(project, obj, history): - serialized = serializers.HistoryExportSerializer(data=history, context={"project": project}) - if serialized.is_valid(): - serialized.object.key = make_key_from_model_object(obj) - if serialized.object.diff is None: - serialized.object.diff = [] - serialized.object._importing = True - serialized.save() - return serialized - add_errors("history", serialized.errors) - return serialized + validator = validators.HistoryExportValidator(data=history, context={"project": project}) + if validator.is_valid(): + validator.object.key = make_key_from_model_object(obj) + if validator.object.diff is None: + validator.object.diff = [] + validator.object.project_id = project.id + validator.object._importing = True + validator.save() + return validator + add_errors("history", validator.errors) + return validator ## ROLES def _store_role(project, role): - serialized = serializers.RoleExportSerializer(data=role) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - return serialized - add_errors("roles", serialized.errors) + validator = validators.RoleExportValidator(data=role) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator + add_errors("roles", validator.errors) return None def store_roles(project, data): results = [] for role in data.get("roles", []): - serialized = _store_role(project, role) - if serialized: - results.append(serialized) + validator = _store_role(project, role) + if validator: + results.append(validator) return results @@ -188,17 +189,17 @@ def store_roles(project, data): ## MEMGERSHIPS def _store_membership(project, membership): - serialized = serializers.MembershipExportSerializer(data=membership, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.object.token = str(uuid.uuid1()) - serialized.object.user = find_invited_user(serialized.object.email, - default=serialized.object.user) - serialized.save() - return serialized + validator = validators.MembershipExportValidator(data=membership, context={"project": project}) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.object.token = str(uuid.uuid1()) + validator.object.user = find_invited_user(validator.object.email, + default=validator.object.user) + validator.save() + return validator - add_errors("memberships", serialized.errors) + add_errors("memberships", validator.errors) return None @@ -212,13 +213,13 @@ def store_memberships(project, data): ## PROJECT ATTRIBUTES def _store_project_attribute_value(project, data, field, serializer): - serialized = serializer(data=data) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - return serialized.object - add_errors(field, serialized.errors) + validator = serializer(data=data) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator.object + add_errors(field, validator.errors) return None @@ -253,13 +254,13 @@ def store_default_project_attributes_values(project, data): ## CUSTOM ATTRIBUTES def _store_custom_attribute(project, data, field, serializer): - serialized = serializer(data=data) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - return serialized.object - add_errors(field, serialized.errors) + validator = serializer(data=data) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator.object + add_errors(field, validator.errors) return None @@ -273,19 +274,19 @@ def store_custom_attributes(project, data, field, serializer): ## MILESTONE def store_milestone(project, milestone): - serialized = serializers.MilestoneExportSerializer(data=milestone, project=project) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - serialized.save_watchers() + validator = validators.MilestoneExportValidator(data=milestone, project=project) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + validator.save_watchers() for task_without_us in milestone.get("tasks_without_us", []): task_without_us["user_story"] = None store_task(project, task_without_us) - return serialized + return validator - add_errors("milestones", serialized.errors) + add_errors("milestones", validator.errors) return None @@ -300,20 +301,20 @@ def store_milestones(project, data): ## USER STORIES def _store_role_point(project, us, role_point): - serialized = serializers.RolePointsExportSerializer(data=role_point, context={"project": project}) - if serialized.is_valid(): + validator = validators.RolePointsExportValidator(data=role_point, context={"project": project}) + if validator.is_valid(): try: - existing_role_point = us.role_points.get(role=serialized.object.role) - existing_role_point.points = serialized.object.points + existing_role_point = us.role_points.get(role=validator.object.role) + existing_role_point.points = validator.object.points existing_role_point.save() return existing_role_point except RolePoints.DoesNotExist: - serialized.object.user_story = us - serialized.save() - return serialized.object + validator.object.user_story = us + validator.save() + return validator.object - add_errors("role_points", serialized.errors) + add_errors("role_points", validator.errors) return None def store_user_story(project, data): @@ -322,51 +323,51 @@ def store_user_story(project, data): us_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]} - serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project}) + validator = validators.UserStoryExportValidator(data=us_data, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True + if validator.is_valid(): + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object._not_notify = True - serialized.save() - serialized.save_watchers() + validator.save() + validator.save_watchers() - if serialized.object.ref: + if validator.object.ref: sequence_name = refs.make_sequence_name(project) if not seq.exists(sequence_name): seq.create(sequence_name) - seq.set_max(sequence_name, serialized.object.ref) + seq.set_max(sequence_name, validator.object.ref) else: - serialized.object.ref, _ = refs.make_reference(serialized.object, project) - serialized.object.save() + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() for us_attachment in data.get("attachments", []): - _store_attachment(project, serialized.object, us_attachment) + _store_attachment(project, validator.object, us_attachment) for role_point in data.get("role_points", []): - _store_role_point(project, serialized.object, role_point) + _store_role_point(project, validator.object, role_point) history_entries = data.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: - custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name') + custom_attributes = validator.object.project.userstorycustomattributes.all().values('id', 'name') custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values( custom_attributes, custom_attributes_values) - _store_custom_attributes_values(serialized.object, custom_attributes_values, - "user_story", serializers.UserStoryCustomAttributesValuesExportSerializer) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "user_story", validators.UserStoryCustomAttributesValuesExportValidator) - return serialized + return validator - add_errors("user_stories", serialized.errors) + add_errors("user_stories", validator.errors) return None @@ -384,47 +385,47 @@ def store_task(project, data): if "status" not in data and project.default_task_status: data["status"] = project.default_task_status.name - serialized = serializers.TaskExportSerializer(data=data, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True + validator = validators.TaskExportValidator(data=data, context={"project": project}) + if validator.is_valid(): + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object._not_notify = True - serialized.save() - serialized.save_watchers() + validator.save() + validator.save_watchers() - if serialized.object.ref: + if validator.object.ref: sequence_name = refs.make_sequence_name(project) if not seq.exists(sequence_name): seq.create(sequence_name) - seq.set_max(sequence_name, serialized.object.ref) + seq.set_max(sequence_name, validator.object.ref) else: - serialized.object.ref, _ = refs.make_reference(serialized.object, project) - serialized.object.save() + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() for task_attachment in data.get("attachments", []): - _store_attachment(project, serialized.object, task_attachment) + _store_attachment(project, validator.object, task_attachment) history_entries = data.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: - custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name') + custom_attributes = validator.object.project.taskcustomattributes.all().values('id', 'name') custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values( custom_attributes, custom_attributes_values) - _store_custom_attributes_values(serialized.object, custom_attributes_values, - "task", serializers.TaskCustomAttributesValuesExportSerializer) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "task", validators.TaskCustomAttributesValuesExportValidator) - return serialized + return validator - add_errors("tasks", serialized.errors) + add_errors("tasks", validator.errors) return None @@ -439,7 +440,7 @@ def store_tasks(project, data): ## ISSUES def store_issue(project, data): - serialized = serializers.IssueExportSerializer(data=data, context={"project": project}) + validator = validators.IssueExportValidator(data=data, context={"project": project}) if "type" not in data and project.default_issue_type: data["type"] = project.default_issue_type.name @@ -453,46 +454,46 @@ def store_issue(project, data): if "severity" not in data and project.default_severity: data["severity"] = project.default_severity.name - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True + if validator.is_valid(): + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object._not_notify = True - serialized.save() - serialized.save_watchers() + validator.save() + validator.save_watchers() - if serialized.object.ref: + if validator.object.ref: sequence_name = refs.make_sequence_name(project) if not seq.exists(sequence_name): seq.create(sequence_name) - seq.set_max(sequence_name, serialized.object.ref) + seq.set_max(sequence_name, validator.object.ref) else: - serialized.object.ref, _ = refs.make_reference(serialized.object, project) - serialized.object.save() + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() for attachment in data.get("attachments", []): - _store_attachment(project, serialized.object, attachment) + _store_attachment(project, validator.object, attachment) history_entries = data.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: - custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name') + custom_attributes = validator.object.project.issuecustomattributes.all().values('id', 'name') custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values( custom_attributes, custom_attributes_values) - _store_custom_attributes_values(serialized.object, custom_attributes_values, - "issue", serializers.IssueCustomAttributesValuesExportSerializer) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "issue", validators.IssueCustomAttributesValuesExportValidator) - return serialized + return validator - add_errors("issues", serialized.errors) + add_errors("issues", validator.errors) return None @@ -507,29 +508,29 @@ def store_issues(project, data): def store_wiki_page(project, wiki_page): wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", ""))) - serialized = serializers.WikiPageExportSerializer(data=wiki_page) - if serialized.is_valid(): - serialized.object.project = project - if serialized.object.owner is None: - serialized.object.owner = serialized.object.project.owner - serialized.object._importing = True - serialized.object._not_notify = True - serialized.save() - serialized.save_watchers() + validator = validators.WikiPageExportValidator(data=wiki_page) + if validator.is_valid(): + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object._not_notify = True + validator.save() + validator.save_watchers() for attachment in wiki_page.get("attachments", []): - _store_attachment(project, serialized.object, attachment) + _store_attachment(project, validator.object, attachment) history_entries = wiki_page.get("history", []) for history in history_entries: - _store_history(project, serialized.object, history) + _store_history(project, validator.object, history) if not history_entries: - take_snapshot(serialized.object, user=serialized.object.owner) + take_snapshot(validator.object, user=validator.object.owner) - return serialized + return validator - add_errors("wiki_pages", serialized.errors) + add_errors("wiki_pages", validator.errors) return None @@ -543,14 +544,14 @@ def store_wiki_pages(project, data): ## WIKI LINKS def store_wiki_link(project, wiki_link): - serialized = serializers.WikiLinkExportSerializer(data=wiki_link) - if serialized.is_valid(): - serialized.object.project = project - serialized.object._importing = True - serialized.save() - return serialized + validator = validators.WikiLinkExportValidator(data=wiki_link) + if validator.is_valid(): + validator.object.project = project + validator.object._importing = True + validator.save() + return validator - add_errors("wiki_links", serialized.errors) + add_errors("wiki_links", validator.errors) return None @@ -572,17 +573,17 @@ def store_tags_colors(project, data): ## TIMELINE def _store_timeline_entry(project, timeline): - serialized = serializers.TimelineExportSerializer(data=timeline, context={"project": project}) - if serialized.is_valid(): - serialized.object.project = project - serialized.object.namespace = build_project_namespace(project) - serialized.object.object_id = project.id - serialized.object.content_type = ContentType.objects.get_for_model(project.__class__) - serialized.object._importing = True - serialized.save() - return serialized - add_errors("timeline", serialized.errors) - return serialized + validator = validators.TimelineExportValidator(data=timeline, context={"project": project}) + if validator.is_valid(): + validator.object.project = project + validator.object.namespace = build_project_namespace(project) + validator.object.object_id = project.id + validator.object.content_type = ContentType.objects.get_for_model(project.__class__) + validator.object._importing = True + validator.save() + return validator + add_errors("timeline", validator.errors) + return validator def store_timeline_entries(project, data): @@ -617,13 +618,13 @@ def _validate_if_owner_have_enought_space_to_this_project(owner, data): def _create_project_object(data): # Create the project - project_serialized = store_project(data) + project_validator = store_project(data) - if not project_serialized: + if not project_validator: errors = get_errors(clear=True) raise err.TaigaImportError(_("error importing project data"), None, errors=errors) - return project_serialized.object if project_serialized else None + return project_validator.object if project_validator else None def _create_membership_for_project_owner(project): @@ -654,13 +655,13 @@ def _populate_project_object(project, data): check_if_there_is_some_error(_("error importing memberships"), project) # Create project attributes values - store_project_attributes_values(project, data, "us_statuses", serializers.UserStoryStatusExportSerializer) - store_project_attributes_values(project, data, "points", serializers.PointsExportSerializer) - store_project_attributes_values(project, data, "task_statuses", serializers.TaskStatusExportSerializer) - store_project_attributes_values(project, data, "issue_types", serializers.IssueTypeExportSerializer) - store_project_attributes_values(project, data, "issue_statuses", serializers.IssueStatusExportSerializer) - store_project_attributes_values(project, data, "priorities", serializers.PriorityExportSerializer) - store_project_attributes_values(project, data, "severities", serializers.SeverityExportSerializer) + store_project_attributes_values(project, data, "us_statuses", validators.UserStoryStatusExportValidator) + store_project_attributes_values(project, data, "points", validators.PointsExportValidator) + store_project_attributes_values(project, data, "task_statuses", validators.TaskStatusExportValidator) + store_project_attributes_values(project, data, "issue_types", validators.IssueTypeExportValidator) + store_project_attributes_values(project, data, "issue_statuses", validators.IssueStatusExportValidator) + store_project_attributes_values(project, data, "priorities", validators.PriorityExportValidator) + store_project_attributes_values(project, data, "severities", validators.SeverityExportValidator) check_if_there_is_some_error(_("error importing lists of project attributes"), project) # Create default values for project attributes @@ -669,11 +670,11 @@ def _populate_project_object(project, data): # Create custom attributes store_custom_attributes(project, data, "userstorycustomattributes", - serializers.UserStoryCustomAttributeExportSerializer) + validators.UserStoryCustomAttributeExportValidator) store_custom_attributes(project, data, "taskcustomattributes", - serializers.TaskCustomAttributeExportSerializer) + validators.TaskCustomAttributeExportValidator) store_custom_attributes(project, data, "issuecustomattributes", - serializers.IssueCustomAttributeExportSerializer) + validators.IssueCustomAttributeExportValidator) check_if_there_is_some_error(_("error importing custom attributes"), project) # Create milestones diff --git a/taiga/export_import/validators/__init__.py b/taiga/export_import/validators/__init__.py new file mode 100644 index 00000000..969a8d0c --- /dev/null +++ b/taiga/export_import/validators/__init__.py @@ -0,0 +1,27 @@ +from .validators import PointsExportValidator +from .validators import UserStoryStatusExportValidator +from .validators import TaskStatusExportValidator +from .validators import IssueStatusExportValidator +from .validators import PriorityExportValidator +from .validators import SeverityExportValidator +from .validators import IssueTypeExportValidator +from .validators import RoleExportValidator +from .validators import UserStoryCustomAttributeExportValidator +from .validators import TaskCustomAttributeExportValidator +from .validators import IssueCustomAttributeExportValidator +from .validators import BaseCustomAttributesValuesExportValidator +from .validators import UserStoryCustomAttributesValuesExportValidator +from .validators import TaskCustomAttributesValuesExportValidator +from .validators import IssueCustomAttributesValuesExportValidator +from .validators import MembershipExportValidator +from .validators import RolePointsExportValidator +from .validators import MilestoneExportValidator +from .validators import TaskExportValidator +from .validators import UserStoryExportValidator +from .validators import IssueExportValidator +from .validators import WikiPageExportValidator +from .validators import WikiLinkExportValidator +from .validators import TimelineExportValidator +from .validators import ProjectExportValidator +from .mixins import AttachmentExportValidator +from .mixins import HistoryExportValidator diff --git a/taiga/export_import/validators/cache.py b/taiga/export_import/validators/cache.py new file mode 100644 index 00000000..c4eb5bfa --- /dev/null +++ b/taiga/export_import/validators/cache.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.users import models as users_models + +_cache_user_by_pk = {} +_cache_user_by_email = {} +_custom_tasks_attributes_cache = {} +_custom_issues_attributes_cache = {} +_custom_userstories_attributes_cache = {} + + +def cached_get_user_by_pk(pk): + if pk not in _cache_user_by_pk: + try: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + except Exception: + _cache_user_by_pk[pk] = users_models.User.objects.get(pk=pk) + return _cache_user_by_pk[pk] + +def cached_get_user_by_email(email): + if email not in _cache_user_by_email: + try: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + except Exception: + _cache_user_by_email[email] = users_models.User.objects.get(email=email) + return _cache_user_by_email[email] diff --git a/taiga/export_import/validators/fields.py b/taiga/export_import/validators/fields.py new file mode 100644 index 00000000..e3d33c7a --- /dev/null +++ b/taiga/export_import/validators/fields.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import base64 +import copy + +from django.core.files.base import ContentFile +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext as _ +from django.contrib.contenttypes.models import ContentType + +from taiga.base.api import serializers +from taiga.base.exceptions import ValidationError +from taiga.base.fields import JsonField +from taiga.mdrender.service import render as mdrender +from taiga.users import models as users_models + +from .cache import cached_get_user_by_email + + +class FileField(serializers.WritableField): + read_only = False + + def from_native(self, data): + if not data: + return None + + decoded_data = b'' + # The original file was encoded by chunks but we don't really know its + # length or if it was multiple of 3 so we must iterate over all those chunks + # decoding them one by one + for decoding_chunk in data['data'].split("="): + # When encoding to base64 3 bytes are transformed into 4 bytes and + # the extra space of the block is filled with = + # We must ensure that the decoding chunk has a length multiple of 4 so + # we restore the stripped '='s adding appending them until the chunk has + # a length multiple of 4 + decoding_chunk += "=" * (-len(decoding_chunk) % 4) + decoded_data += base64.b64decode(decoding_chunk + "=") + + return ContentFile(decoded_data, name=data['name']) + + +class ContentTypeField(serializers.RelatedField): + read_only = False + + def from_native(self, data): + try: + return ContentType.objects.get_by_natural_key(*data) + except Exception: + return None + + +class RelatedNoneSafeField(serializers.RelatedField): + 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() + + key = self.source or field_name + if value in self.null_values: + if self.required: + raise ValidationError(self.error_messages['required']) + into[key] = None + elif self.many: + into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None] + else: + into[key] = self.from_native(value) + + +class UserRelatedField(RelatedNoneSafeField): + read_only = False + + def from_native(self, data): + try: + return cached_get_user_by_email(data) + except users_models.User.DoesNotExist: + return None + + +class UserPkField(serializers.RelatedField): + read_only = False + + def from_native(self, data): + try: + user = cached_get_user_by_email(data) + return user.pk + except users_models.User.DoesNotExist: + return None + + +class CommentField(serializers.WritableField): + read_only = False + + def field_from_native(self, data, files, field_name, into): + super().field_from_native(data, files, field_name, into) + into["comment_html"] = mdrender(self.context['project'], data.get("comment", "")) + + +class ProjectRelatedField(serializers.RelatedField): + read_only = False + null_values = (None, "") + + def __init__(self, slug_field, *args, **kwargs): + self.slug_field = slug_field + super().__init__(*args, **kwargs) + + def from_native(self, data): + try: + 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))) + + +class HistoryUserField(JsonField): + def from_native(self, data): + if data is None: + return {} + + if len(data) < 2: + return {} + + user = UserRelatedField().from_native(data[0]) + + if user: + pk = user.pk + else: + pk = None + + return {"pk": pk, "name": data[1]} + + +class HistoryValuesField(JsonField): + def from_native(self, data): + if data is None: + return [] + if "users" in data: + data['users'] = list(map(UserPkField().from_native, data['users'])) + return data + + +class HistoryDiffField(JsonField): + def from_native(self, data): + if data is None: + return [] + + if "assigned_to" in data: + data['assigned_to'] = list(map(UserPkField().from_native, data['assigned_to'])) + return data + + +class TimelineDataField(serializers.WritableField): + read_only = False + + def from_native(self, data): + new_data = copy.deepcopy(data) + try: + user = cached_get_user_by_email(new_data["user"]["email"]) + new_data["user"]["id"] = user.id + del new_data["user"]["email"] + except users_models.User.DoesNotExist: + pass + + return new_data diff --git a/taiga/export_import/validators/mixins.py b/taiga/export_import/validators/mixins.py new file mode 100644 index 00000000..d07334b6 --- /dev/null +++ b/taiga/export_import/validators/mixins.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.contenttypes.models import ContentType + +from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.projects.history import models as history_models +from taiga.projects.attachments import models as attachments_models +from taiga.projects.notifications import services as notifications_services +from taiga.projects.history import services as history_service + +from .fields import (UserRelatedField, HistoryUserField, HistoryDiffField, + JsonField, HistoryValuesField, CommentField, FileField) + + +class HistoryExportValidator(validators.ModelValidator): + user = HistoryUserField() + diff = HistoryDiffField(required=False) + snapshot = JsonField(required=False) + values = HistoryValuesField(required=False) + comment = CommentField(required=False) + delete_comment_date = serializers.DateTimeField(required=False) + delete_comment_user = HistoryUserField(required=False) + + class Meta: + model = history_models.HistoryEntry + exclude = ("id", "comment_html", "key", "project") + + +class AttachmentExportValidator(validators.ModelValidator): + owner = UserRelatedField(required=False) + attached_file = FileField() + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = attachments_models.Attachment + exclude = ('id', 'content_type', 'object_id', 'project') + + +class WatcheableObjectModelValidatorMixin(validators.ModelValidator): + watchers = UserRelatedField(many=True, required=False) + + def __init__(self, *args, **kwargs): + self._watchers_field = self.base_fields.pop("watchers", None) + super(WatcheableObjectModelValidatorMixin, self).__init__(*args, **kwargs) + + """ + watchers is not a field from the model so we need to do some magic to make it work like a normal field + It's supposed to be represented as an email list but internally it's treated like notifications.Watched instances + """ + + def restore_object(self, attrs, instance=None): + self.fields.pop("watchers", None) + instance = super(WatcheableObjectModelValidatorMixin, self).restore_object(attrs, instance) + self._watchers = self.init_data.get("watchers", []) + return instance + + def save_watchers(self): + new_watcher_emails = set(self._watchers) + old_watcher_emails = set(self.object.get_watchers().values_list("email", flat=True)) + adding_watcher_emails = list(new_watcher_emails.difference(old_watcher_emails)) + removing_watcher_emails = list(old_watcher_emails.difference(new_watcher_emails)) + + User = get_user_model() + adding_users = User.objects.filter(email__in=adding_watcher_emails) + removing_users = User.objects.filter(email__in=removing_watcher_emails) + + for user in adding_users: + notifications_services.add_watcher(self.object, user) + + for user in removing_users: + notifications_services.remove_watcher(self.object, user) + + self.object.watchers = [user.email for user in self.object.get_watchers()] + + def to_native(self, obj): + ret = super(WatcheableObjectModelValidatorMixin, self).to_native(obj) + ret["watchers"] = [user.email for user in obj.get_watchers()] + return ret diff --git a/taiga/export_import/validators/validators.py b/taiga/export_import/validators/validators.py new file mode 100644 index 00000000..818df0c3 --- /dev/null +++ b/taiga/export_import/validators/validators.py @@ -0,0 +1,349 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 as _ + +from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.fields import JsonField, PgArrayField +from taiga.base.exceptions import ValidationError + +from taiga.projects import models as projects_models +from taiga.projects.custom_attributes import models as custom_attributes_models +from taiga.projects.userstories import models as userstories_models +from taiga.projects.tasks import models as tasks_models +from taiga.projects.issues import models as issues_models +from taiga.projects.milestones import models as milestones_models +from taiga.projects.wiki import models as wiki_models +from taiga.timeline import models as timeline_models +from taiga.users import models as users_models + +from .fields import (FileField, UserRelatedField, + ProjectRelatedField, + TimelineDataField, ContentTypeField) +from .mixins import WatcheableObjectModelValidatorMixin +from .cache import (_custom_tasks_attributes_cache, + _custom_userstories_attributes_cache, + _custom_issues_attributes_cache) + + +class PointsExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.Points + exclude = ('id', 'project') + + +class UserStoryStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.UserStoryStatus + exclude = ('id', 'project') + + +class TaskStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.TaskStatus + exclude = ('id', 'project') + + +class IssueStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.IssueStatus + exclude = ('id', 'project') + + +class PriorityExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.Priority + exclude = ('id', 'project') + + +class SeverityExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.Severity + exclude = ('id', 'project') + + +class IssueTypeExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.IssueType + exclude = ('id', 'project') + + +class RoleExportValidator(validators.ModelValidator): + permissions = PgArrayField(required=False) + + class Meta: + model = users_models.Role + exclude = ('id', 'project') + + +class UserStoryCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.UserStoryCustomAttribute + exclude = ('id', 'project') + + +class TaskCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.TaskCustomAttribute + exclude = ('id', 'project') + + +class IssueCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.IssueCustomAttribute + exclude = ('id', 'project') + + +class BaseCustomAttributesValuesExportValidator(validators.ModelValidator): + attributes_values = JsonField(source="attributes_values", required=True) + _custom_attribute_model = None + _container_field = None + + class Meta: + exclude = ("id",) + + def validate_attributes_values(self, attrs, source): + # values must be a dict + data_values = attrs.get("attributes_values", None) + if self.object: + data_values = (data_values or self.object.attributes_values) + + if type(data_values) is not dict: + raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}")) + + # Values keys must be in the container object project + data_container = attrs.get(self._container_field, None) + if data_container: + project_id = data_container.project_id + elif self.object: + project_id = getattr(self.object, self._container_field).project_id + else: + project_id = None + + values_ids = list(data_values.keys()) + qs = self._custom_attribute_model.objects.filter(project=project_id, + id__in=values_ids) + if qs.count() != len(values_ids): + raise ValidationError(_("It contain invalid custom fields.")) + + return attrs + + +class UserStoryCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute + _container_model = "userstories.UserStory" + _container_field = "user_story" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.UserStoryCustomAttributesValues + + +class TaskCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.TaskCustomAttribute + _container_field = "task" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.TaskCustomAttributesValues + + +class IssueCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.IssueCustomAttribute + _container_field = "issue" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.IssueCustomAttributesValues + + +class MembershipExportValidator(validators.ModelValidator): + user = UserRelatedField(required=False) + role = ProjectRelatedField(slug_field="name") + invited_by = UserRelatedField(required=False) + + class Meta: + model = projects_models.Membership + exclude = ('id', 'project', 'token') + + def full_clean(self, instance): + return instance + + +class RolePointsExportValidator(validators.ModelValidator): + role = ProjectRelatedField(slug_field="name") + points = ProjectRelatedField(slug_field="name") + + class Meta: + model = userstories_models.RolePoints + exclude = ('id', 'user_story') + + +class MilestoneExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + modified_date = serializers.DateTimeField(required=False) + estimated_start = serializers.DateField(required=False) + estimated_finish = serializers.DateField(required=False) + + def __init__(self, *args, **kwargs): + project = kwargs.pop('project', None) + super(MilestoneExportValidator, self).__init__(*args, **kwargs) + if project: + self.project = project + + def validate_name(self, attrs, source): + """ + Check the milestone name is not duplicated in the project + """ + name = attrs[source] + qs = self.project.milestones.filter(name=name) + if qs.exists(): + raise ValidationError(_("Name duplicated for the project")) + + return attrs + + class Meta: + model = milestones_models.Milestone + exclude = ('id', 'project') + + +class TaskExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + user_story = ProjectRelatedField(slug_field="ref", required=False) + milestone = ProjectRelatedField(slug_field="name", required=False) + assigned_to = UserRelatedField(required=False) + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = tasks_models.Task + exclude = ('id', 'project') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_tasks_attributes_cache: + _custom_tasks_attributes_cache[project.id] = list(project.taskcustomattributes.all().values('id', 'name')) + return _custom_tasks_attributes_cache[project.id] + + +class UserStoryExportValidator(WatcheableObjectModelValidatorMixin): + role_points = RolePointsExportValidator(many=True, required=False) + owner = UserRelatedField(required=False) + assigned_to = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + milestone = ProjectRelatedField(slug_field="name", required=False) + modified_date = serializers.DateTimeField(required=False) + generated_from_issue = ProjectRelatedField(slug_field="ref", required=False) + + class Meta: + model = userstories_models.UserStory + exclude = ('id', 'project', 'points', 'tasks') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_userstories_attributes_cache: + _custom_userstories_attributes_cache[project.id] = list( + project.userstorycustomattributes.all().values('id', 'name') + ) + return _custom_userstories_attributes_cache[project.id] + + +class IssueExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + assigned_to = UserRelatedField(required=False) + priority = ProjectRelatedField(slug_field="name") + severity = ProjectRelatedField(slug_field="name") + type = ProjectRelatedField(slug_field="name") + milestone = ProjectRelatedField(slug_field="name", required=False) + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = issues_models.Issue + exclude = ('id', 'project') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_issues_attributes_cache: + _custom_issues_attributes_cache[project.id] = list(project.issuecustomattributes.all().values('id', 'name')) + return _custom_issues_attributes_cache[project.id] + + +class WikiPageExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + last_modifier = UserRelatedField(required=False) + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = wiki_models.WikiPage + exclude = ('id', 'project') + + +class WikiLinkExportValidator(validators.ModelValidator): + class Meta: + model = wiki_models.WikiLink + exclude = ('id', 'project') + + +class TimelineExportValidator(validators.ModelValidator): + data = TimelineDataField() + data_content_type = ContentTypeField() + + class Meta: + model = timeline_models.Timeline + exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') + + +class ProjectExportValidator(WatcheableObjectModelValidatorMixin): + logo = FileField(required=False) + anon_permissions = PgArrayField(required=False) + public_permissions = PgArrayField(required=False) + modified_date = serializers.DateTimeField(required=False) + roles = RoleExportValidator(many=True, required=False) + owner = UserRelatedField(required=False) + memberships = MembershipExportValidator(many=True, required=False) + points = PointsExportValidator(many=True, required=False) + us_statuses = UserStoryStatusExportValidator(many=True, required=False) + task_statuses = TaskStatusExportValidator(many=True, required=False) + issue_types = IssueTypeExportValidator(many=True, required=False) + issue_statuses = IssueStatusExportValidator(many=True, required=False) + priorities = PriorityExportValidator(many=True, required=False) + severities = SeverityExportValidator(many=True, required=False) + tags_colors = JsonField(required=False) + creation_template = serializers.SlugRelatedField(slug_field="slug", required=False) + default_points = serializers.SlugRelatedField(slug_field="name", required=False) + default_us_status = serializers.SlugRelatedField(slug_field="name", required=False) + default_task_status = serializers.SlugRelatedField(slug_field="name", required=False) + default_priority = serializers.SlugRelatedField(slug_field="name", required=False) + default_severity = serializers.SlugRelatedField(slug_field="name", required=False) + default_issue_status = serializers.SlugRelatedField(slug_field="name", required=False) + default_issue_type = serializers.SlugRelatedField(slug_field="name", required=False) + userstorycustomattributes = UserStoryCustomAttributeExportValidator(many=True, required=False) + taskcustomattributes = TaskCustomAttributeExportValidator(many=True, required=False) + issuecustomattributes = IssueCustomAttributeExportValidator(many=True, required=False) + user_stories = UserStoryExportValidator(many=True, required=False) + tasks = TaskExportValidator(many=True, required=False) + milestones = MilestoneExportValidator(many=True, required=False) + issues = IssueExportValidator(many=True, required=False) + wiki_links = WikiLinkExportValidator(many=True, required=False) + wiki_pages = WikiPageExportValidator(many=True, required=False) + + class Meta: + model = projects_models.Project + exclude = ('id', 'members') From 0a900cc84dd7035a75afd7a770d43a6a88fd54b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 26 Jul 2016 13:23:27 +0200 Subject: [PATCH 124/261] Fixed race condition on multiple attachment deletion --- taiga/projects/attachments/api.py | 2 +- taiga/projects/history/mixins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 27f7ebe1..986c8f19 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -74,7 +74,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, # NOTE: When destroy an attachment, the content_object change # after and not before self.persist_history_snapshot(obj, delete=True) - super().pre_delete(obj) + super().post_delete(obj) def get_object_for_snapshot(self, obj): return obj.content_object diff --git a/taiga/projects/history/mixins.py b/taiga/projects/history/mixins.py index 0a70366d..14a0f44d 100644 --- a/taiga/projects/history/mixins.py +++ b/taiga/projects/history/mixins.py @@ -62,7 +62,7 @@ class HistoryResourceMixin(object): obj = self.get_object() sobj = self.get_object_for_snapshot(obj) - if sobj != obj and delete: + if sobj != obj: delete = False notifications_services.analize_object_for_watchers(obj, comment, user) From 5214717856a95328c6cec569a03421625af4ffb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 27 Jul 2016 11:56:41 +0200 Subject: [PATCH 125/261] Fix some of the deadlocks on take_snapshot --- taiga/projects/history/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 764cca39..b9526b08 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -312,7 +312,7 @@ def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False """ key = make_key_from_model_object(obj) - with advisory_lock(key): + with advisory_lock("history-"+key): typename = get_typename_for_model_class(obj.__class__) new_fobj = freeze_model_instance(obj) From 5eb6bfe1e09c0dcf30cecc1f91642dca224feaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 27 Jul 2016 12:44:02 +0200 Subject: [PATCH 126/261] Only update tasks milestone when needed --- taiga/projects/userstories/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py index 11638595..fc1fdacc 100644 --- a/taiga/projects/userstories/signals.py +++ b/taiga/projects/userstories/signals.py @@ -59,7 +59,7 @@ def update_role_points_when_create_or_edit_us(sender, instance, **kwargs): def update_milestone_of_tasks_when_edit_us(sender, instance, created, **kwargs): if not created: - instance.tasks.update(milestone=instance.milestone) + instance.tasks.exclude(milestone=instance.milestone).update(milestone=instance.milestone) for task in instance.tasks.all(): take_snapshot(task) From 3e9bfd7523bcd83416a879e1c4bca347a576821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 27 Jul 2016 13:16:36 +0200 Subject: [PATCH 127/261] Fixing slug duplications on race conditions --- taiga/base/decorators.py | 4 +-- taiga/projects/api.py | 24 ++++++++++-------- taiga/projects/milestones/models.py | 13 +++++----- taiga/projects/models.py | 38 ++++++++++++++--------------- taiga/projects/wiki/models.py | 12 ++++++--- taiga/users/models.py | 30 ++++++++++++----------- 6 files changed, 65 insertions(+), 56 deletions(-) diff --git a/taiga/base/decorators.py b/taiga/base/decorators.py index 5700e75b..46b80b24 100644 --- a/taiga/base/decorators.py +++ b/taiga/base/decorators.py @@ -18,6 +18,7 @@ from django_pglocks import advisory_lock + def detail_route(methods=['get'], **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. @@ -51,12 +52,11 @@ def model_pk_lock(func): """ def decorator(self, *args, **kwargs): from taiga.base.utils.db import get_typename_for_model_class - lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field pk = self.kwargs.get(self.pk_url_kwarg, None) tn = get_typename_for_model_class(self.get_queryset().model) key = "{0}:{1}".format(tn, pk) - with advisory_lock(key) as acquired_key_lock: + with advisory_lock(key): return func(self, *args, **kwargs) return decorator diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 6445c17f..89fdffe2 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -26,6 +26,8 @@ from django.http import Http404 from django.utils.translation import ugettext as _ from django.utils import timezone +from django_pglocks import advisory_lock + from taiga.base import filters from taiga.base import exceptions as exc from taiga.base import response @@ -214,20 +216,22 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, if not template_description: raise response.BadRequest(_("Not valid template description")) - template_slug = slugify_uniquely(template_name, models.ProjectTemplate) + with advisory_lock("create-project-template") as acquired_key_lock: + template_slug = slugify_uniquely(template_name, models.ProjectTemplate) - project = self.get_object() + project = self.get_object() - self.check_permissions(request, 'create_template', project) + self.check_permissions(request, 'create_template', project) - template = models.ProjectTemplate( - name=template_name, - slug=template_slug, - description=template_description, - ) + template = models.ProjectTemplate( + name=template_name, + slug=template_slug, + description=template_description, + ) - template.load_data_from_project(project) - template.save() + template.load_data_from_project(project) + + template.save() return response.Created(serializers.ProjectTemplateSerializer(template).data) @detail_route(methods=['POST']) diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index 21d85b14..4488d178 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -16,9 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.apps import apps from django.db import models -from django.db.models import Prefetch, Count +from django.db.models import Count from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -28,7 +27,7 @@ from django.utils.functional import cached_property from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.dicts import dict_sum from taiga.projects.notifications.mixins import WatchedModelMixin -from taiga.projects.userstories.models import UserStory +from django_pglocks import advisory_lock import itertools import datetime @@ -84,9 +83,11 @@ class Milestone(WatchedModelMixin, models.Model): if not self._importing or not self.modified_date: self.modified_date = timezone.now() if not self.slug: - self.slug = slugify_uniquely(self.name, self.__class__) - - super().save(*args, **kwargs) + with advisory_lock("milestone-creation-{}".format(self.project_id)): + self.slug = slugify_uniquely(self.name, self.__class__) + super().save(*args, **kwargs) + else: + super().save(*args, **kwargs) @cached_property def cached_user_stories(self): diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 83e36e54..2c0f4027 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -16,27 +16,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import itertools -import uuid - from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models -from django.db.models import signals, Q +from django.db.models import Q from django.apps import apps -from django.conf import settings -from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.utils.functional import cached_property +from django_pglocks import advisory_lock + from django_pgjson.fields import JsonField from taiga.projects.tagging.models import TaggedMixin from taiga.projects.tagging.models import TagsColorsdMixin -from taiga.base.utils.dicts import dict_sum from taiga.base.utils.files import get_file_path from taiga.base.utils.sequence import arithmetic_progression from taiga.base.utils.slug import slugify_uniquely @@ -270,16 +266,6 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): if not self._importing or not self.modified_date: self.modified_date = timezone.now() - if not self.slug: - base_name = "{}-{}".format(self.owner.username, self.name) - base_slug = slugify_uniquely(base_name, self.__class__) - slug = base_slug - for i in arithmetic_progression(): - if not type(self).objects.filter(slug=slug).exists() or i > 100: - break - slug = "{}-{}".format(base_slug, i) - self.slug = slug - if not self.is_backlog_activated: self.total_milestones = None self.total_story_points = None @@ -290,13 +276,25 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): if not self.is_looking_for_people: self.looking_for_people_note = "" - if self.anon_permissions == None: + if self.anon_permissions is None: self.anon_permissions = [] - if self.public_permissions == None: + if self.public_permissions is None: self.public_permissions = [] - super().save(*args, **kwargs) + if not self.slug: + with advisory_lock("project-creation"): + base_name = "{}-{}".format(self.owner.username, self.name) + base_slug = slugify_uniquely(base_name, self.__class__) + slug = base_slug + for i in arithmetic_progression(): + if not type(self).objects.filter(slug=slug).exists() or i > 100: + break + slug = "{}-{}".format(base_slug, i) + self.slug = slug + super().save(*args, **kwargs) + else: + super().save(*args, **kwargs) def refresh_totals(self, save=True): now = timezone.now() diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index 19cd6b25..5a4b3485 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -21,6 +21,8 @@ from django.contrib.contenttypes.fields import GenericRelation from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django_pglocks import advisory_lock + from taiga.base.utils.slug import slugify_uniquely_for_queryset from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.occ import OCCModelMixin @@ -84,7 +86,9 @@ class WikiLink(models.Model): def save(self, *args, **kwargs): if not self.href: - wl_qs = self.project.wiki_links.all() - self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href") - - super().save(*args, **kwargs) + with advisory_lock("wiki-page-creation-{}".format(self.project_id)): + wl_qs = self.project.wiki_links.all() + self.href = slugify_uniquely_for_queryset(self.title, wl_qs, slugfield="href") + super().save(*args, **kwargs) + else: + super().save(*args, **kwargs) diff --git a/taiga/users/models.py b/taiga/users/models.py index b9c60e4b..98a71dc8 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -35,6 +35,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django_pgjson.fields import JsonField +from django_pglocks import advisory_lock from taiga.auth.tokens import get_token_for_user from taiga.base.utils.slug import slugify_uniquely @@ -265,20 +266,21 @@ class User(AbstractBaseUser, PermissionsMixin): super().save(*args, **kwargs) def cancel(self): - self.username = slugify_uniquely("deleted-user", User, slugfield="username") - self.email = "{}@taiga.io".format(self.username) - self.is_active = False - self.full_name = "Deleted user" - self.color = "" - self.bio = "" - self.lang = "" - self.theme = "" - self.timezone = "" - self.colorize_tags = True - self.token = None - self.set_unusable_password() - self.photo = None - self.save() + with advisory_lock("delete-user"): + self.username = slugify_uniquely("deleted-user", User, slugfield="username") + self.email = "{}@taiga.io".format(self.username) + self.is_active = False + self.full_name = "Deleted user" + self.color = "" + self.bio = "" + self.lang = "" + self.theme = "" + self.timezone = "" + self.colorize_tags = True + self.token = None + self.set_unusable_password() + self.photo = None + self.save() self.auth_data.all().delete() # Blocking all owned projects From 0f15f671f5dd1bd642a5b1aee1c8124fd9cc6718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 28 Jul 2016 12:45:45 +0200 Subject: [PATCH 128/261] US#4445: Add milestones to project detail --- taiga/projects/serializers.py | 8 ++++++++ taiga/projects/utils.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 1d8a5799..a24281c7 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -356,6 +356,14 @@ class ProjectDetailSerializer(ProjectSerializer): tasks_csv_uuid = Field() userstories_csv_uuid = Field() transfer_token = Field() + milestones = MethodField() + + def get_milestones(self, obj): + assert hasattr(obj, "milestones_attr"), "instance must have a milestones_attr attribute" + if obj.milestones_attr is None: + return [] + + return obj.milestones_attr def to_value(self, instance): # Name attributes must be translated diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py index ee552136..d8c46072 100644 --- a/taiga/projects/utils.py +++ b/taiga/projects/utils.py @@ -50,6 +50,31 @@ def attach_members(queryset, as_field="members_attr"): return queryset +def attach_milestones(queryset, as_field="milestones_attr"): + """Attach a json milestons representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the milestones as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(t)) + FROM( + SELECT + milestones_milestone.id, + milestones_milestone.slug, + milestones_milestone.name, + milestones_milestone.closed + FROM milestones_milestone + WHERE milestones_milestone.project_id = {tbl}.id + ORDER BY estimated_start) t""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + def attach_closed_milestones(queryset, as_field="closed_milestones_attr"): """Attach a closed milestones counter to each object of the queryset. @@ -432,5 +457,6 @@ def attach_extra_info(queryset, user=None): queryset = attach_my_role_permissions(queryset, user) queryset = attach_private_projects_same_owner(queryset, user) queryset = attach_public_projects_same_owner(queryset, user) + queryset = attach_milestones(queryset) return queryset From 04fb04340a08ce3444ac04f4ff01b90e0632d311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 29 Jul 2016 12:40:54 +0200 Subject: [PATCH 129/261] Prevent the creation of a duplicate gogs user if you have the gogs plugin enabled --- taiga/hooks/gogs/migrations/0001_initial.py | 26 +++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/taiga/hooks/gogs/migrations/0001_initial.py b/taiga/hooks/gogs/migrations/0001_initial.py index 09ba6709..5c35b081 100644 --- a/taiga/hooks/gogs/migrations/0001_initial.py +++ b/taiga/hooks/gogs/migrations/0001_initial.py @@ -15,18 +15,20 @@ def create_gogs_system_user(apps, schema_editor): # if we directly import it, it'll be the wrong version User = apps.get_model("users", "User") db_alias = schema_editor.connection.alias - random_hash = uuid.uuid4().hex - user = User.objects.using(db_alias).create( - username="gogs-{}".format(random_hash), - email="gogs-{}@taiga.io".format(random_hash), - full_name="Gogs", - is_active=False, - is_system=True, - bio="", - ) - f = open("{}/logo.png".format(CUR_DIR), "rb") - user.photo.save("logo.png", File(f)) - user.save() + + if not User.objects.using(db_alias).filter(is_system=True, username__startswith="gogs-").exists(): + random_hash = uuid.uuid4().hex + user = User.objects.using(db_alias).create( + username="gogs-{}".format(random_hash), + email="gogs-{}@taiga.io".format(random_hash), + full_name="Gogs", + is_active=False, + is_system=True, + bio="", + ) + f = open("{}/logo.png".format(CUR_DIR), "rb") + user.photo.save("logo.png", File(f)) + user.save() class Migration(migrations.Migration): From 73599c41ffbc4e51a788c9298b3eb21fc95e3fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Sun, 31 Jul 2016 12:19:05 +0200 Subject: [PATCH 130/261] [i18n] Update locales --- taiga/locale/es/LC_MESSAGES/django.po | 130 +++++++++++++---- taiga/locale/ru/LC_MESSAGES/django.po | 200 +++++++++++++++++++------- 2 files changed, 253 insertions(+), 77 deletions(-) diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index ec287179..00614517 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -8,6 +8,7 @@ # gustavodiazjaimes , 2015 # Hector Colina , 2015 # Jesus Marin , 2015 +# Jorge Sanchez , 2016 # Luis Sebastian Urrutia Fuentes , 2016 # Renelis Abreu Ramirez , 2016 # Taiga Dev Team , 2015-2016 @@ -17,8 +18,8 @@ msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" -"Last-Translator: Taiga Dev Team \n" +"PO-Revision-Date: 2016-07-12 19:06+0000\n" +"Last-Translator: Jorge Sanchez \n" "Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/es/)\n" "MIME-Version: 1.0\n" @@ -362,7 +363,7 @@ msgstr "Error por incumplimiento de precondición" #: taiga/base/exceptions.py:217 msgid "No room left for more projects." -msgstr "" +msgstr "No hay espacio para mas proyectos" #: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." @@ -600,7 +601,7 @@ msgstr "error importando los timelines" #: taiga/export_import/services/store.py:731 msgid "unexpected error importing project" -msgstr "" +msgstr "Error inesperado al importar el proyecto" #: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" @@ -625,6 +626,21 @@ msgid "" "TRACE ERROR:\n" "------------" msgstr "" +"\n" +"\n" +"Error cargando importacion {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" #: taiga/export_import/tasks.py:110 msgid "Error loading project dump" @@ -632,11 +648,11 @@ msgstr "Error cargando el volcado de datos del proyecto" #: taiga/export_import/tasks.py:111 msgid "Error loading your project dump file" -msgstr "" +msgstr "Error cargando el archivo del proyecto exportado" #: taiga/export_import/tasks.py:125 msgid " -- no detail info --" -msgstr "" +msgstr "-- sin informacion --" #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format @@ -1386,7 +1402,7 @@ msgstr "El usuario no existe" #: taiga/projects/api.py:366 msgid "The user must be already a project member" -msgstr "" +msgstr "El usuario debe ser un miembro del proyecto" #: taiga/projects/api.py:672 msgid "" @@ -1479,15 +1495,15 @@ msgstr "Talky" #: taiga/projects/choices.py:32 msgid "This project is blocked due to payment failure" -msgstr "" +msgstr "El proyecto esta bloqueado por un fallo en el pago" #: taiga/projects/choices.py:33 msgid "This project is blocked by admin staff" -msgstr "" +msgstr "El proyecto esta bloqueado por los administradores" #: taiga/projects/choices.py:34 msgid "This project is blocked because the owner left" -msgstr "" +msgstr "El proyecto esta bloqueado porque el dueño ha salido" #: taiga/projects/custom_attributes/choices.py:27 msgid "Text" @@ -2794,11 +2810,12 @@ msgstr "Rol inválido para el proyecto" #: taiga/projects/serializers.py:195 msgid "The project owner must be admin." -msgstr "" +msgstr "El dueño del proyecto debe ser administrador" #: taiga/projects/serializers.py:198 msgid "At least one user must be an active admin for this project." msgstr "" +"Por lo menos un usuario debe ser administrador activo para este proyecto" #: taiga/projects/serializers.py:396 msgid "Default options" @@ -2838,11 +2855,11 @@ msgstr "Roles" #: taiga/projects/services/members.py:116 msgid "You have reached your current limit of memberships for private projects" -msgstr "" +msgstr "Ha alcanzado el limite de miembros para proyectos privados" #: taiga/projects/services/members.py:120 msgid "You have reached your current limit of memberships for public projects" -msgstr "" +msgstr "Ha alcanzado el limite de miembros para proyectos públicos" #: taiga/projects/services/projects.py:69 #: taiga/projects/services/projects.py:106 taiga/users/services.py:582 @@ -2854,6 +2871,7 @@ msgstr "No puedes tener más proyectos privados" msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" +"Este proyecto alcanzo el limite actual de miembros para proyectos privados" #: taiga/projects/services/projects.py:77 #: taiga/projects/services/projects.py:114 taiga/users/services.py:589 @@ -2865,6 +2883,7 @@ msgstr "No puedes tener más proyectos públicos" msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" +"Este proyecto alcanzo su limite actual de miembros para proyectos publicos" #: taiga/projects/services/stats.py:196 msgid "Future sprint" @@ -3068,11 +3087,16 @@ msgid "" "new project owner for \"%(project_name)s\".

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

Hola %(old_owner_name)s,

\n" +"

%(new_owner_name)s acepto su oferta y sera el nuevo dueño del " +"proyecto \"%(project_name)s\".

\n" +" " #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 #, python-format msgid "

%(new_owner_name)s says:

" -msgstr "" +msgstr "

%(new_owner_name)s dice:

" #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 msgid "" @@ -3081,6 +3105,9 @@ msgid "" "p>\n" " " msgstr "" +"\n" +"

Desde ahora su nuevo estado para este proyecto es \"admin\".

\n" +" " #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 #, python-format @@ -3098,7 +3125,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 #, python-format msgid "%(new_owner_name)s says:" -msgstr "" +msgstr "%(new_owner_name)s dice:" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 msgid "" @@ -3137,6 +3164,11 @@ msgid "" "new project owner for \"%(project_name)s\".

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

Hola %(owner_name)s,

\n" +"

%(rejecter_name)s declino su oferta y no sera el nuevo dueño del " +"proyecto \"%(project_name)s\".

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 #, python-format @@ -3145,6 +3177,9 @@ msgid "" "

%(rejecter_name)s says:

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

%(rejecter_name)s dice:

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 msgid "" @@ -3153,6 +3188,10 @@ msgid "" "different person.

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

Si lo desea, aun puede transferir el dominio del proyecto a otra " +"persona.

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 @@ -3175,7 +3214,7 @@ msgstr "" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 #, python-format msgid "%(rejecter_name)s says:" -msgstr "" +msgstr "%(rejecter_name)s dice:" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 msgid "" @@ -3210,6 +3249,11 @@ msgid "" "\"%(project_name)s\".

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

Hola %(owner_name)s,

\n" +"

%(requester_name)s ha solicitado convertirse en el dueño del " +"proyecto \"%(project_name)s\".

\n" +" " #: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 msgid "" @@ -3218,6 +3262,10 @@ msgid "" "project transfer from the administration panel.

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

Por favor, Haga click en \"Continuar\" Si desea iniciar la " +"transferencia del proyecto desde el panel de administracion.

\n" +" " #: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 #: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 @@ -3232,6 +3280,10 @@ msgid "" "%(requester_name)s has requested to become the project owner for " "\"%(project_name)s\".\n" msgstr "" +"\n" +"Hola %(owner_name)s,\n" +"%(requester_name)s ha solicitado ser el dueño del proyecto \"%(project_name)s" +"\".\n" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 msgid "" @@ -3239,10 +3291,13 @@ msgid "" "Please, go to your project settings if you would like to start the project " "transfer from the administration panel.\n" msgstr "" +"\n" +"Por favor, vaya a la configuracion del proyecto si desea iniciar la " +"tranferencia del proyecto desde el panel de administracion.\n" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 msgid "Go to your project settings:" -msgstr "" +msgstr "Ir a la configuracion del proyecto:" #: taiga/projects/templates/emails/transfer_request-subject.jinja:1 #, python-format @@ -3250,6 +3305,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer request\n" msgstr "" +"\n" +"[%(project)s] Solicitud de transferencia de dominio del proyecto\n" #: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 #, python-format @@ -3260,6 +3317,11 @@ msgid "" "would like you to become the new project owner.

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

Hola %(receiver_name)s,

\n" +"

%(owner_name)s, el dueño del proyecto \"%(project_name)s\" desea " +"que usted sea el nuevo dueño del proyecto.

\n" +" " #: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 #, python-format @@ -3268,6 +3330,9 @@ msgid "" "

%(owner_name)s says:

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

%(owner_name)s dice:

\n" +" " #: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 msgid "" @@ -3276,6 +3341,10 @@ msgid "" "proposal.

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

Por favor, Haga click en \"Continuar\" Para aceptar o rechazar " +"esta propuesta.

\n" +" " #: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 #, python-format @@ -3285,11 +3354,15 @@ msgid "" "%(owner_name)s, the current project owner at \"%(project_name)s\" would like " "you to become the new project owner.\n" msgstr "" +"\n" +"Hola %(receiver_name)s,\n" +"%(owner_name)s, el dueño del proyecto \"%(project_name)s\" desea que usted " +"sea el nuevo dueño del proyecto\n" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 #, python-format msgid "%(owner_name)s says:" -msgstr "" +msgstr "%(owner_name)s dice:" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 msgid "" @@ -3297,10 +3370,13 @@ msgid "" "Please, go to the following link to either accept or reject this proposal.\n" msgstr "" +"\n" +"Por favor, Vaya al siguiente link para aceptar o rechazar esta propuesta.\n" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 msgid "Accept or reject the project ownership transfer:" -msgstr "" +msgstr "Aceptar o rechazar la transferencia del dominio del proyecto:" #: taiga/projects/templates/emails/transfer_start-subject.jinja:1 #, python-format @@ -3308,6 +3384,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer offer\n" msgstr "" +"\n" +"[%(project)s] Oferta de transferencia de dominio del proyecto\n" #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 @@ -3646,23 +3724,23 @@ msgstr "Comprueba la API de histórico para obtener el diff exacto" #: taiga/users/admin.py:38 msgid "Project Member" -msgstr "" +msgstr "Miembro del proyecto" #: taiga/users/admin.py:39 msgid "Project Members" -msgstr "" +msgstr "Miembros del proyecto" #: taiga/users/admin.py:49 msgid "id" -msgstr "" +msgstr "Id" #: taiga/users/admin.py:81 msgid "Project Ownership" -msgstr "" +msgstr "Dueño del proyecto" #: taiga/users/admin.py:82 msgid "Project Ownerships" -msgstr "" +msgstr "Dueños del proyecto" #: taiga/users/admin.py:119 msgid "Personal info" @@ -3797,11 +3875,11 @@ msgstr "nueva dirección de email" #: taiga/users/models.py:167 msgid "max number of owned private projects" -msgstr "" +msgstr "numero maximo de proyectos privados asignados" #: taiga/users/models.py:170 msgid "max number of owned public projects" -msgstr "" +msgstr "numero maximo de proyectos publicos asignados" #: taiga/users/models.py:173 msgid "max number of memberships for each owned private project" diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po index 366ee566..d4164285 100644 --- a/taiga/locale/ru/LC_MESSAGES/django.po +++ b/taiga/locale/ru/LC_MESSAGES/django.po @@ -7,6 +7,7 @@ # Dmitriy Volkov , 2015 # Dmitry Lobanov , 2015 # Dmitry Vinokurov , 2015 +# Egor Poderyagin , 2016 # Igor Bezukladnikov , 2016 # ilyar, 2016 # ivan tkachenko , 2016 @@ -16,8 +17,8 @@ msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" -"Last-Translator: Taiga Dev Team \n" +"PO-Revision-Date: 2016-07-26 04:45+0000\n" +"Last-Translator: Egor Poderyagin \n" "Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ru/)\n" "MIME-Version: 1.0\n" @@ -205,7 +206,7 @@ msgstr "" #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 #: taiga/webhooks/api.py:68 msgid "Blocked element" -msgstr "" +msgstr "Заблокированный элемент" #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." @@ -358,7 +359,7 @@ msgstr "Ошибка предусловия" #: taiga/base/exceptions.py:217 msgid "No room left for more projects." -msgstr "" +msgstr "Не осталось места для проектов" #: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." @@ -523,7 +524,7 @@ msgstr "Нам была нужна хотя бы одна роль" #: taiga/export_import/api.py:309 msgid "Needed dump file" -msgstr "Необходим дамп-файл" +msgstr "Необходим дамп" #: taiga/export_import/api.py:316 msgid "Invalid dump format" @@ -607,7 +608,7 @@ msgstr "ошибка импорта хронологии проекта" #: taiga/export_import/services/store.py:731 msgid "unexpected error importing project" -msgstr "" +msgstr "неожиданная ошибка импортирования проекта" #: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" @@ -632,18 +633,33 @@ msgid "" "TRACE ERROR:\n" "------------" msgstr "" +"\n" +"\n" +"Ошибка загрузки дампа {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"ПРИЧИНА:\n" +"-------\n" +"{reason}\n" +"\n" +"ДЕТАЛИ:\n" +"--------\n" +"{details}\n" +"\n" +"ТРАССИРОВКА ОШИБКИ:\n" +"------------" #: taiga/export_import/tasks.py:110 msgid "Error loading project dump" -msgstr "Ошибка загрузки свалочного файла проекта" +msgstr "Ошибка загрузки дампа" #: taiga/export_import/tasks.py:111 msgid "Error loading your project dump file" -msgstr "" +msgstr "Ошибка загрузки дампа вашего проекта" #: taiga/export_import/tasks.py:125 msgid " -- no detail info --" -msgstr "" +msgstr "-- нет детальной информации --" #: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 #, python-format @@ -1386,21 +1402,23 @@ msgstr "Неверное описание шаблона" #: taiga/projects/api.py:356 msgid "Invalid user id" -msgstr "" +msgstr "Неправильный id пользователя" #: taiga/projects/api.py:362 msgid "The user doesn't exist" -msgstr "" +msgstr "Пользователь не существует" #: taiga/projects/api.py:366 msgid "The user must be already a project member" -msgstr "" +msgstr "Пользователь должен быть участником проекта" #: taiga/projects/api.py:672 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" +"У проекта должен быть владелец и по крайней мере один пользователь должен " +"быть активным администратором" #: taiga/projects/api.py:706 msgid "You don't have permisions to see that." @@ -1485,15 +1503,15 @@ msgstr "Talky" #: taiga/projects/choices.py:32 msgid "This project is blocked due to payment failure" -msgstr "" +msgstr "Проект заблокирован из-за ошибки при оплате" #: taiga/projects/choices.py:33 msgid "This project is blocked by admin staff" -msgstr "" +msgstr "Проект заблокирован администраторами" #: taiga/projects/choices.py:34 msgid "This project is blocked because the owner left" -msgstr "" +msgstr "Проект заблокирован, потому-что владелец ушёл" #: taiga/projects/custom_attributes/choices.py:27 msgid "Text" @@ -1509,7 +1527,7 @@ msgstr "Дата" #: taiga/projects/custom_attributes/choices.py:30 msgid "Url" -msgstr "" +msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 #: taiga/projects/issues/models.py:47 @@ -1902,15 +1920,15 @@ msgstr "личное" #: taiga/projects/models.py:201 msgid "is featured" -msgstr "" +msgstr "особенность" #: taiga/projects/models.py:204 msgid "is looking for people" -msgstr "" +msgstr "ищут людей" #: taiga/projects/models.py:206 msgid "loking for people note" -msgstr "" +msgstr "ищем замечания людей" #: taiga/projects/models.py:218 msgid "tags colors" @@ -1918,11 +1936,11 @@ msgstr "цвета тэгов" #: taiga/projects/models.py:221 msgid "project transfer token" -msgstr "" +msgstr "токен передачи проекта" #: taiga/projects/models.py:225 msgid "blocked code" -msgstr "" +msgstr "заблокированный код" #: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 msgid "updated date time" @@ -1935,15 +1953,15 @@ msgstr "количество" #: taiga/projects/models.py:235 msgid "fans last week" -msgstr "" +msgstr "фанатов на прошлой недели " #: taiga/projects/models.py:238 msgid "fans last month" -msgstr "" +msgstr "фанатов в прошлом месяце" #: taiga/projects/models.py:241 msgid "fans last year" -msgstr "" +msgstr "фанатов в прошлом году" #: taiga/projects/models.py:247 msgid "activity last week" @@ -2801,6 +2819,7 @@ msgstr "версия" msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" +"Вы не можете покинуть проект, если вы владелец или нет других администраторов" #: taiga/projects/serializers.py:172 msgid "Email address is already taken" @@ -2812,11 +2831,13 @@ msgstr "Неверная роль для этого проекта" #: taiga/projects/serializers.py:195 msgid "The project owner must be admin." -msgstr "" +msgstr "Владелец проекта должен быть администратором" #: taiga/projects/serializers.py:198 msgid "At least one user must be an active admin for this project." msgstr "" +"По крайней мере один пользователь должен быть администратором для этого " +"проекта" #: taiga/projects/serializers.py:396 msgid "Default options" @@ -2856,33 +2877,33 @@ msgstr "Роли" #: taiga/projects/services/members.py:116 msgid "You have reached your current limit of memberships for private projects" -msgstr "" +msgstr "Вы достигли лимита участников для частного проекта" #: taiga/projects/services/members.py:120 msgid "You have reached your current limit of memberships for public projects" -msgstr "" +msgstr "Вы достигли лимита участников для публичного проекта" #: taiga/projects/services/projects.py:69 #: taiga/projects/services/projects.py:106 taiga/users/services.py:582 msgid "You can't have more private projects" -msgstr "" +msgstr "Вы не можете иметь больше частных проектов" #: taiga/projects/services/projects.py:73 #: taiga/projects/services/projects.py:110 taiga/users/services.py:585 msgid "" "This project reaches your current limit of memberships for private projects" -msgstr "" +msgstr "В этом частном проекте достигнут лимит участников" #: taiga/projects/services/projects.py:77 #: taiga/projects/services/projects.py:114 taiga/users/services.py:589 msgid "You can't have more public projects" -msgstr "" +msgstr "Вы не можете иметь больше публичных проектов" #: taiga/projects/services/projects.py:81 #: taiga/projects/services/projects.py:118 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for public projects" -msgstr "" +msgstr "В этом публичном проекте достигнут лимит участников" #: taiga/projects/services/stats.py:196 msgid "Future sprint" @@ -2901,7 +2922,7 @@ msgstr "Неверный токен" #: taiga/projects/services/transfer.py:66 msgid "Token has expired" -msgstr "" +msgstr "Срок действия токена истёк" #: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 msgid "You don't have permissions to set this sprint to this task." @@ -3092,11 +3113,16 @@ msgid "" "new project owner for \"%(project_name)s\".

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

Привет %(old_owner_name)s,

\n" +"

%(new_owner_name)s подтвердил ваше предложение и будет новым владельцем " +"для \"%(project_name)s\".

\n" +" " #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 #, python-format msgid "

%(new_owner_name)s says:

" -msgstr "" +msgstr "

%(new_owner_name)s сказал:

" #: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 msgid "" @@ -3105,6 +3131,9 @@ msgid "" "p>\n" " " msgstr "" +"\n" +"

С этого момента Ваш статус будет администратор.

\n" +" " #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 #, python-format @@ -3114,17 +3143,23 @@ msgid "" "%(new_owner_name)s has accepted your offer and will become the new project " "owner for \"%(project_name)s\".\n" msgstr "" +"\n" +"Привет %(old_owner_name)s,\n" +"%(new_owner_name)s подтвердил ваше предложение и будет новым владельцем для " +"\"%(project_name)s\".\n" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 #, python-format msgid "%(new_owner_name)s says:" -msgstr "" +msgstr "%(new_owner_name)s сказал:" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 msgid "" "\n" "From now on, your new status for this project will be \"admin\".\n" msgstr "" +"\n" +"С этого момента Ваш статус будет администратор.\n" #: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 @@ -3134,6 +3169,8 @@ msgid "" "\n" "The Taiga Team\n" msgstr "" +"\n" +"The Taiga Team\n" #: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 #, python-format @@ -3141,6 +3178,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer offer accepted!\n" msgstr "" +"\n" +"[%(project)s] Передача проекта подтверждена\n" #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 #, python-format @@ -3151,6 +3190,10 @@ msgid "" "new project owner for \"%(project_name)s\".

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

Привет %(owner_name)s,

\n" +"

%(rejecter_name)s отменил ваше предложение и не будет новым владельцем " +"для \"%(project_name)s\".

" #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 #, python-format @@ -3159,6 +3202,9 @@ msgid "" "

%(rejecter_name)s says:

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

%(rejecter_name)s сказал:

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 msgid "" @@ -3167,11 +3213,15 @@ msgid "" "different person.

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

Если Вы хотите, Вы можете попробовать передать собственность другой " +"персоне.

\n" +" " #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 #: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 msgid "Request transfer to a different person" -msgstr "" +msgstr "Запрос передан другой персоне" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 #, python-format @@ -3181,11 +3231,15 @@ msgid "" "%(rejecter_name)s has declined your offer and will not become the new " "project owner for \"%(project_name)s\".\n" msgstr "" +"\n" +"Привет %(owner_name)s,\n" +"%(rejecter_name)s отменил ваше предложение и не будет новым владельцем для " +"\"%(project_name)s\".\n" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 #, python-format msgid "%(rejecter_name)s says:" -msgstr "" +msgstr "%(rejecter_name)s сказал:" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 msgid "" @@ -3193,10 +3247,13 @@ msgid "" "If you want, you can still try to transfer the project ownership to a " "different person.\n" msgstr "" +"\n" +"Если Вы хотите, Вы можете попробовать передать собственность другой " +"персоне.\n" #: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 msgid "Request transfer to a different person:" -msgstr "" +msgstr "Запрос передан другой персоне:" #: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 #, python-format @@ -3204,6 +3261,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer declined\n" msgstr "" +"\n" +"[%(project)s] Передача проекта отменена\n" #: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 #, python-format @@ -3214,6 +3273,11 @@ msgid "" "\"%(project_name)s\".

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

Привет %(owner_name)s,

\n" +"

%(requester_name)s просит назначить его владельцем проекта для " +"\"%(project_name)s\".

\n" +" " #: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 msgid "" @@ -3222,11 +3286,15 @@ msgid "" "project transfer from the administration panel.

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

Пожалуйста, нажмите \"Продолжить\" если вы хотите начать передачу проекта " +"из панели администратора.

\n" +" " #: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 #: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 msgid "Continue" -msgstr "" +msgstr "Продолжить" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 #, python-format @@ -3236,6 +3304,10 @@ msgid "" "%(requester_name)s has requested to become the project owner for " "\"%(project_name)s\".\n" msgstr "" +"\n" +"Привет %(owner_name)s,\n" +"%(requester_name)s просит назначить его владельцем проекта для " +"\"%(project_name)s\".\n" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 msgid "" @@ -3243,10 +3315,13 @@ msgid "" "Please, go to your project settings if you would like to start the project " "transfer from the administration panel.\n" msgstr "" +"\n" +"Пожалуйста, перейдите в настройки проекта, если Вы хотите начать передачу " +"проекта из панели администратора.\n" #: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 msgid "Go to your project settings:" -msgstr "" +msgstr "Перейдите в настройки проекта:" #: taiga/projects/templates/emails/transfer_request-subject.jinja:1 #, python-format @@ -3254,6 +3329,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer request\n" msgstr "" +"\n" +"[%(project)s] Запрос передачи проекта\n" #: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 #, python-format @@ -3264,6 +3341,11 @@ msgid "" "would like you to become the new project owner.

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

Привет %(receiver_name)s,

\n" +"

%(owner_name)s, текущий владелец \"%(project_name)s\", хотел что бы Вы " +"стали новым владельцем проекта.

\n" +" " #: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 #, python-format @@ -3272,6 +3354,9 @@ msgid "" "

%(owner_name)s says:

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

%(owner_name)s сказал:

\n" +" " #: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 msgid "" @@ -3280,6 +3365,10 @@ msgid "" "proposal.

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

Пожалуйста, нажмите \"Продолжить\", для принятия или отклонения " +"предложения

\n" +" " #: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 #, python-format @@ -3289,11 +3378,15 @@ msgid "" "%(owner_name)s, the current project owner at \"%(project_name)s\" would like " "you to become the new project owner.\n" msgstr "" +"\n" +"Привет %(receiver_name)s,\n" +"%(owner_name)s, владелец \"%(project_name)s\", хотел что бы Вы стали новым " +"владельцем проекта.\n" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 #, python-format msgid "%(owner_name)s says:" -msgstr "" +msgstr "%(owner_name)s сказал:" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 msgid "" @@ -3301,10 +3394,13 @@ msgid "" "Please, go to the following link to either accept or reject this proposal.\n" msgstr "" +"\n" +"Пожалуйста, пройдите по следующей ссылке для принятия или отклонения " +"предложения.

\n" #: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 msgid "Accept or reject the project ownership transfer:" -msgstr "" +msgstr "Подтвердите или отклоните передачу владения проектом:" #: taiga/projects/templates/emails/transfer_start-subject.jinja:1 #, python-format @@ -3312,6 +3408,8 @@ msgid "" "\n" "[%(project)s] Project ownership transfer offer\n" msgstr "" +"\n" +"[%(project)s] Предложение передачи проекта\n" #. Translators: Name of scrum project template. #: taiga/projects/translations.py:29 @@ -3647,23 +3745,23 @@ msgstr "Свертесть с историей API для получения и #: taiga/users/admin.py:38 msgid "Project Member" -msgstr "" +msgstr "Участник проекта" #: taiga/users/admin.py:39 msgid "Project Members" -msgstr "" +msgstr "Участники проекта" #: taiga/users/admin.py:49 msgid "id" -msgstr "" +msgstr "id" #: taiga/users/admin.py:81 msgid "Project Ownership" -msgstr "" +msgstr "Владелец проекта" #: taiga/users/admin.py:82 msgid "Project Ownerships" -msgstr "" +msgstr "Владельцы проекта" #: taiga/users/admin.py:119 msgid "Personal info" @@ -3675,7 +3773,7 @@ msgstr "Права доступа" #: taiga/users/admin.py:123 msgid "Restrictions" -msgstr "" +msgstr "Ограничения" #: taiga/users/admin.py:125 msgid "Important dates" @@ -3793,19 +3891,19 @@ msgstr "новый email адрес" #: taiga/users/models.py:167 msgid "max number of owned private projects" -msgstr "" +msgstr "максимальное число частных проектов" #: taiga/users/models.py:170 msgid "max number of owned public projects" -msgstr "" +msgstr "максимальное число публичных проектов" #: taiga/users/models.py:173 msgid "max number of memberships for each owned private project" -msgstr "" +msgstr "максимальное число участников для каждого частного проекта" #: taiga/users/models.py:177 msgid "max number of memberships for each owned public project" -msgstr "" +msgstr "максимальное число участников для каждого публичного проекта" #: taiga/users/models.py:297 msgid "permissions" From d1f7158125f9a68475b5b74fc7e1e6a46514b4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 2 Aug 2016 13:27:56 +0200 Subject: [PATCH 131/261] Fix bulk_update_orders for user stories --- taiga/projects/userstories/validators.py | 32 +++++---- tests/integration/test_userstories.py | 91 +++++++++++++++++++++++- 2 files changed, 109 insertions(+), 14 deletions(-) diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index ba470456..e82b1f75 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -36,15 +36,6 @@ from . import models import json -class UserStoryExistsValidator: - def validate_us_id(self, attrs, source): - value = attrs[source] - if not models.UserStory.objects.filter(pk=value).exists(): - msg = _("There's no user story with that id") - raise ValidationError(msg) - return attrs - - class RolePointsField(serializers.WritableField): def to_native(self, obj): return {str(o.role.id): o.points.id for o in obj.all()} @@ -76,7 +67,7 @@ class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsVali # Order bulk validators -class _UserStoryOrderBulkValidator(UserStoryExistsValidator, validators.Validator): +class _UserStoryOrderBulkValidator(validators.Validator): us_id = serializers.IntegerField() order = serializers.IntegerField() @@ -88,10 +79,25 @@ class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatu milestone_id = serializers.IntegerField(required=False) bulk_stories = _UserStoryOrderBulkValidator(many=True) + def validate(self, data): + filters = {"project__id": data["project_id"]} + if "status_id" in data: + filters["status__id"] = data["status_id"] + if "milestone_id" in data: + filters["milestone__id"] = data["milestone_id"] + + filters["id__in"] = [us["us_id"] for us in data["bulk_stories"]] + + if models.UserStory.objects.filter(**filters).count() != len(filters["id__in"]): + raise ValidationError(_("Invalid user story ids. All stories must belong to the same project and, " + "if it exists, to the same status and milestone.")) + + return data + # Milestone bulk validators -class _UserStoryMilestoneBulkValidator(UserStoryExistsValidator, validators.Validator): +class _UserStoryMilestoneBulkValidator(validators.Validator): us_id = serializers.IntegerField() @@ -108,9 +114,9 @@ class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValida project = get_object_or_404(Project, pk=data["project_id"]) if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): - raise ValidationError("all the user stories must be from the same project") + raise ValidationError(_("All the user stories must be from the same project")) if project.milestones.filter(id=data["milestone_id"]).count() != 1: - raise ValidationError("the milestone isn't valid for the project") + raise ValidationError(_("The milestone isn't valid for the project")) return data diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 1d3984bd..bfb86abc 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -180,6 +180,96 @@ def test_api_update_orders_in_bulk(client): assert response3.status_code == 200, response3.data +def test_api_update_orders_in_bulk_invalid_userstories(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project) + us3 = f.create_userstory() + + url1 = reverse("userstories-bulk-update-backlog-order") + url2 = reverse("userstories-bulk-update-kanban-order") + url3 = reverse("userstories-bulk-update-sprint-order") + + data = { + "project_id": project.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}, + {"us_id": us3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + response3 = client.json.post(url3, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + assert response3.status_code == 400, response3.data + + +def test_api_update_orders_in_bulk_invalid_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + us2 = f.create_userstory(project=project, status=us1.status) + us3 = f.create_userstory(project=project) + + url1 = reverse("userstories-bulk-update-backlog-order") + url2 = reverse("userstories-bulk-update-kanban-order") + url3 = reverse("userstories-bulk-update-sprint-order") + + data = { + "project_id": project.id, + "status_id": us1.status.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}, + {"us_id": us3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + response3 = client.json.post(url3, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + assert response3.status_code == 400, response3.data + + +def test_api_update_orders_in_bulk_invalid_milestione(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create(project=project) + us1 = f.create_userstory(project=project, milestone=mil1) + us2 = f.create_userstory(project=project, milestone=mil1) + us3 = f.create_userstory(project=project) + + url1 = reverse("userstories-bulk-update-backlog-order") + url2 = reverse("userstories-bulk-update-kanban-order") + url3 = reverse("userstories-bulk-update-sprint-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}, + {"us_id": us3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + response3 = client.json.post(url3, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + assert response3.status_code == 400, response3.data + + def test_api_update_milestone_in_bulk(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) @@ -208,7 +298,6 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) us1 = f.create_userstory(project=project) us2 = f.create_userstory(project=project) - f.MilestoneFactory.create(project=project) m2 = f.MilestoneFactory.create() url = reverse("userstories-bulk-update-milestone") From 3187bdfed9861de757761ed5710b4b09181c81ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 2 Aug 2016 15:14:07 +0200 Subject: [PATCH 132/261] Fix tasks validators for bulk operations --- taiga/projects/tasks/validators.py | 70 ++++++-- tests/integration/test_tasks.py | 253 ++++++++++++++++++++++++++++- 2 files changed, 304 insertions(+), 19 deletions(-) diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index 3dd634bd..033b72e3 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -22,23 +22,18 @@ from taiga.base.api import serializers from taiga.base.api import validators from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField -from taiga.projects.milestones.validators import MilestoneExistsValidator +from taiga.projects.milestones.models import Milestone +from taiga.projects.models import TaskStatus from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.userstories.models import UserStory from taiga.projects.validators import ProjectExistsValidator + + from . import models -class TaskExistsValidator: - def validate_task_id(self, attrs, source): - value = attrs[source] - if not models.Task.objects.filter(pk=value).exists(): - msg = _("There's no task with that id") - raise ValidationError(msg) - return attrs - - class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): tags = TagsAndTagsColorsField(default=[], required=False) external_reference = PgArrayField(required=False) @@ -48,25 +43,72 @@ class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, valida read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') -class TasksBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, - TaskExistsValidator, validators.Validator): +class TasksBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() sprint_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) bulk_tasks = serializers.CharField() + def validate_sprint_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + filters["id"] = attrs["sprint_id"] + + if not Milestone.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid sprint id.")) + + return attrs + + def validate_status_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + filters["id"] = attrs["status_id"] + + if not TaskStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid task status id.")) + + return attrs + + def validate_us_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + + if "sprint_id" in attrs: + filters["milestone__id"] = attrs["sprint_id"] + + filters["id"] = attrs["us_id"] + + if not UserStory.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid sprint id.")) + + return attrs + # Order bulk validators -class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator): +class _TaskOrderBulkValidator(validators.Validator): task_id = serializers.IntegerField() order = serializers.IntegerField() class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() - milestone_id = serializers.IntegerField(required=False) status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) + milestone_id = serializers.IntegerField(required=False) bulk_tasks = _TaskOrderBulkValidator(many=True) + + def validate(self, data): + filters = {"project__id": data["project_id"]} + if "status_id" in data: + filters["status__id"] = data["status_id"] + if "us_id" in data: + filters["user_story__id"] = data["us_id"] + if "milestone_id" in data: + filters["milestone__id"] = data["milestone_id"] + + filters["id__in"] = [t["task_id"] for t in data["bulk_tasks"]] + + if models.Task.objects.filter(**filters).count() != len(filters["id__in"]): + raise ValidationError(_("Invalid task ids. All tasks must belong to the same project and, " + "if it exists, to the same status, user story and/or milestone.")) + + return data diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 398a40a2..c13add00 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -85,10 +85,16 @@ def test_create_task_without_default_values(client): assert response.data['status'] == None -def test_api_create_in_bulk_with_status(client): - us = f.create_userstory() - f.MembershipFactory.create(project=us.project, user=us.owner, is_admin=True) - us.project.default_task_status = f.TaskStatusFactory.create(project=us.project) +def test_api_create_in_bulk_with_status_milestone_userstory(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + url = reverse("tasks-bulk-create") data = { "bulk_tasks": "Story #1\nStory #2", @@ -98,13 +104,141 @@ def test_api_create_in_bulk_with_status(client): "status_id": us.project.default_task_status.id } - client.login(us.owner) + client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 200 assert response.data[0]["status"] == us.project.default_task_status.id +def test_api_create_in_bulk_with_status_milestone(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "project_id": us.project.id, + "sprint_id": us.milestone.id, + "status_id": us.project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + assert response.data[0]["status"] == us.project.default_task_status.id + + +def test_api_create_in_bulk_with_invalid_status(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project, milestone=milestone) + + status = f.TaskStatusFactory.create() + + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "sprint_id": milestone.id, + "status_id": status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_api_create_in_bulk_with_invalid_milestone(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory() + us = f.create_userstory(project=project) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "sprint_id": milestone.id, + "status_id": project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_api_create_in_bulk_with_invalid_userstory_1(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory() + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": project.id, + "sprint_id": milestone.id, + "status_id": project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + +def test_api_create_in_bulk_with_invalid_userstory_2(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user, default_task_status=None) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.save() + milestone = f.MilestoneFactory(project=project) + us = f.create_userstory(project=project) + + url = reverse("tasks-bulk-create") + data = { + "bulk_tasks": "Story #1\nStory #2", + "us_id": us.id, + "project_id": us.project.id, + "sprint_id": milestone.id, + "status_id": us.project.default_task_status.id + } + + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + + def test_api_create_invalid_task(client): # Associated to a milestone and a user story. # But the User Story is not associated with the milestone @@ -152,6 +286,115 @@ def test_api_update_order_in_bulk(client): assert response2.status_code == 200, response2.data +def test_api_update_order_in_bulk_invalid_tasks(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task() + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + +def test_api_update_order_in_bulk_invalid_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project, status=task1.status) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "status_id": task1.status.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + +def test_api_update_order_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create(project=project) + task1 = f.create_task(project=project, milestone=mil1) + task2 = f.create_task(project=project, milestone=mil1) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + +def test_api_update_order_in_bulk_invalid_user_story(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory(project=project) + task1 = f.create_task(project=project, user_story=us1) + task2 = f.create_task(project=project, user_story=us1) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 400, response1.data + assert response2.status_code == 400, response2.data + + def test_get_invalid_csv(client): url = reverse("tasks-csv") From baab2552ab3b98a0a1cfe8cee114fe0c35608ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 2 Aug 2016 18:01:12 +0200 Subject: [PATCH 133/261] Fix tests --- tests/integration/test_userstories.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index bfb86abc..6a095f08 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -312,7 +312,7 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert response.data["non_field_errors"][0] == "the milestone isn't valid for the project" + assert len(response.data["non_field_errors"]) == 1 def test_api_update_milestone_in_bulk_invalid_userstories(client): @@ -334,7 +334,7 @@ def test_api_update_milestone_in_bulk_invalid_userstories(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert response.data["non_field_errors"][0] == "all the user stories must be from the same project" + assert len(response.data["non_field_errors"]) == 1 def test_update_userstory_points(client): From 2d4ab32fb45664766821877a8397ba10f67d47fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 2 Aug 2016 12:39:16 +0200 Subject: [PATCH 134/261] Small bugfixes on bad requests --- taiga/projects/history/api.py | 9 +++++++++ taiga/projects/userstories/api.py | 8 +++++++- taiga/projects/userstories/permissions.py | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index 2119239a..db1d6bbc 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -62,6 +62,8 @@ class HistoryViewSet(ReadOnlyListViewSet): obj = self.get_object() history_entry_id = request.QUERY_PARAMS.get('id', None) history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + if history_entry is None: + return response.NotFound() self.check_permissions(request, 'comment_versions', history_entry) @@ -76,6 +78,9 @@ class HistoryViewSet(ReadOnlyListViewSet): obj = self.get_object() history_entry_id = request.QUERY_PARAMS.get('id', None) history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + if history_entry is None: + return response.NotFound() + obj = services.get_instance_from_key(history_entry.key) comment = request.DATA.get("comment", None) @@ -113,6 +118,8 @@ class HistoryViewSet(ReadOnlyListViewSet): obj = self.get_object() history_entry_id = request.QUERY_PARAMS.get('id', None) history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + if history_entry is None: + return response.NotFound() self.check_permissions(request, 'delete_comment', history_entry) @@ -132,6 +139,8 @@ class HistoryViewSet(ReadOnlyListViewSet): obj = self.get_object() history_entry_id = request.QUERY_PARAMS.get('id', None) history_entry = services.get_history_queryset_by_model_instance(obj).filter(id=history_entry_id).first() + if history_entry is None: + return response.NotFound() self.check_permissions(request, 'undelete_comment', history_entry) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 143cb1ea..07e31cfe 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -286,8 +286,14 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi @list_route(methods=["GET"]) def by_ref(self, request): + if "ref" not in request.QUERY_PARAMS: + return response.BadRequest(_("ref param is needed")) + + if "project_slug" not in request.QUERY_PARAMS and "project_id" not in request.QUERY_PARAMS: + return response.BadRequest(_("project_id or project_slug param is needed")) + retrieve_kwargs = { - "ref": request.QUERY_PARAMS.get("ref", None) + "ref": request.QUERY_PARAMS["ref"] } project_id = request.QUERY_PARAMS.get("project", None) if project_id is not None: diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index 6b46f72c..2d200446 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -26,6 +26,7 @@ class UserStoryPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None retrieve_perms = HasProjectPerm('view_us') + by_ref_perms = HasProjectPerm('view_us') create_perms = HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us') update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us') partial_update_perms = CommentAndOrUpdatePerm('modify_us', 'comment_us') From f78159e564f08935eea8ec68685e2b2aeeade46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 3 Aug 2016 08:47:07 +0200 Subject: [PATCH 135/261] Small fix on get user story by ref --- taiga/projects/userstories/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 07e31cfe..61911d61 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -289,8 +289,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi if "ref" not in request.QUERY_PARAMS: return response.BadRequest(_("ref param is needed")) - if "project_slug" not in request.QUERY_PARAMS and "project_id" not in request.QUERY_PARAMS: - return response.BadRequest(_("project_id or project_slug param is needed")) + if "project_slug" not in request.QUERY_PARAMS and "project" not in request.QUERY_PARAMS: + return response.BadRequest(_("project or project_slug param is needed")) retrieve_kwargs = { "ref": request.QUERY_PARAMS["ref"] From cc01ff029c8cd1c1624461dea529e7141f75c784 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 29 Jul 2016 09:27:36 +0200 Subject: [PATCH 136/261] Refactoring bulk update milestone API call --- taiga/projects/userstories/services.py | 18 ++++++++++++++++-- taiga/projects/userstories/validators.py | 1 + tests/integration/test_userstories.py | 24 +++++++++++++++--------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index f1c5d683..0f260e2c 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -105,9 +105,21 @@ def update_userstories_order_in_bulk(bulk_data: list, field: str, project: objec def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object): """ - Update the milestone of some user stories. - `bulk_data` should be a list of user story ids: + Update the milestone and the milestone order of some user stories adding the + extra orders needed to keep consistency. + `bulk_data` should be a list of dicts with the following format: + [{'us_id': , 'order': }, ...] """ + user_stories = milestone.user_stories.all() + us_orders = {us.id: getattr(us, "sprint_order") for us in user_stories} + new_us_orders = {} + for e in bulk_data: + new_us_orders[e["us_id"]] = e["order"] + # The base orders where we apply the new orders must containg all the values + us_orders[e["us_id"]] = e["order"] + + apply_order_updates(us_orders, new_us_orders) + us_milestones = {e["us_id"]: milestone.id for e in bulk_data} user_story_ids = us_milestones.keys() @@ -116,6 +128,8 @@ def update_userstories_milestone_in_bulk(bulk_data: list, milestone: object): projectid=milestone.project.pk) db.update_attr_in_bulk_for_ids(us_milestones, "milestone_id", model=models.UserStory) + db.update_attr_in_bulk_for_ids(us_orders, "sprint_order", models.UserStory) + return us_orders def snapshot_userstories_in_bulk(bulk_data, user): diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index e82b1f75..60bedaf7 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -99,6 +99,7 @@ class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatu class _UserStoryMilestoneBulkValidator(validators.Validator): us_id = serializers.IntegerField() + order = serializers.IntegerField() class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, validators.Validator): diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 6a095f08..b777b186 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -273,24 +273,30 @@ def test_api_update_orders_in_bulk_invalid_milestione(client): def test_api_update_milestone_in_bulk(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + milestone = f.MilestoneFactory.create(project=project) us1 = f.create_userstory(project=project) us2 = f.create_userstory(project=project) - milestone = f.MilestoneFactory.create(project=project) + us3 = f.create_userstory(project=project, milestone=milestone, sprint_order=1) + us4 = f.create_userstory(project=project, milestone=milestone, sprint_order=2) url = reverse("userstories-bulk-update-milestone") data = { "project_id": project.id, "milestone_id": milestone.id, - "bulk_stories": [{"us_id": us1.id}, - {"us_id": us2.id}] + "bulk_stories": [{"us_id": us1.id, "order": 2}, + {"us_id": us2.id, "order": 3}] } client.login(project.owner) - assert project.milestones.get(id=milestone.id).user_stories.count() == 0 + assert project.milestones.get(id=milestone.id).user_stories.count() == 2 response = client.json.post(url, json.dumps(data)) assert response.status_code == 204, response.data - assert project.milestones.get(id=milestone.id).user_stories.count() == 2 + assert project.milestones.get(id=milestone.id).user_stories.count() == 4 + assert list(project.milestones.get(id=milestone.id).\ + user_stories.\ + order_by("sprint_order").\ + values_list("id", "sprint_order")) == [(us3.id, 1), (us1.id, 2), (us2.id,3), (us4.id,4)] def test_api_update_milestone_in_bulk_invalid_milestone(client): @@ -304,8 +310,8 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client): data = { "project_id": project.id, "milestone_id": m2.id, - "bulk_stories": [{"us_id": us1.id}, - {"us_id": us2.id}] + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}] } client.login(project.owner) @@ -326,8 +332,8 @@ def test_api_update_milestone_in_bulk_invalid_userstories(client): data = { "project_id": project.id, "milestone_id": milestone.id, - "bulk_stories": [{"us_id": us1.id}, - {"us_id": us2.id}] + "bulk_stories": [{"us_id": us1.id, "order": 1}, + {"us_id": us2.id, "order": 2}] } client.login(project.owner) From 29a64d1243c54a49a8eb8070ef993c5921369ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Aug 2016 14:33:08 +0200 Subject: [PATCH 137/261] Add role id to the members dict serializer --- taiga/projects/serializers.py | 1 + taiga/projects/utils.py | 1 + 2 files changed, 2 insertions(+) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index a24281c7..ecdae019 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -99,6 +99,7 @@ class IssueTypeSerializer(serializers.LightSerializer): ###################################################### class MembershipDictSerializer(serializers.LightDictSerializer): + role = Field() role_name = Field() full_name = Field() full_name_display = MethodField() diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py index d8c46072..271a511c 100644 --- a/taiga/projects/utils.py +++ b/taiga/projects/utils.py @@ -37,6 +37,7 @@ def attach_members(queryset, as_field="members_attr"): users_user.color, users_user.photo, users_user.is_active, + users_role.id "role", users_role.name role_name FROM projects_membership From c414e6b9c4998bd5fb9f15343a2a3b278fadb096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 17 Aug 2016 10:41:28 +0200 Subject: [PATCH 138/261] Fix role serializer --- taiga/users/serializers.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 5b81ac6f..e2e55ea9 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -19,7 +19,7 @@ from django.conf import settings from taiga.base.api import serializers -from taiga.base.fields import PgArrayField, Field, MethodField, I18NField +from taiga.base.fields import Field, MethodField, I18NField from taiga.base.utils.thumbnails import get_thumbnail_url @@ -138,24 +138,17 @@ class UserBasicInfoSerializer(serializers.LightSerializer): class RoleSerializer(serializers.LightSerializer): id = Field() name = Field() - computable = Field() + slug = Field() project = Field(attr="project_id") order = Field() + computable = Field() + permissions = Field() members_count = MethodField() - permissions = PgArrayField(required=False) def get_members_count(self, obj): return obj.memberships.count() -class ProjectRoleSerializer(serializers.LightSerializer): - id = Field() - name = I18NField() - slug = Field() - order = Field() - computable = Field() - - ###################################################### # Like ###################################################### From a6f5b4c0ec7aca3587da79654f2fecb45c8f8c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 2 Aug 2016 15:50:54 +0200 Subject: [PATCH 139/261] Fix small bugs found while generating api documentation --- .travis.yml | 1 + taiga/base/api/fields.py | 4 +- taiga/projects/attachments/api.py | 3 ++ .../management/commands/sample_data.py | 50 +++++++++---------- taiga/projects/models.py | 1 + taiga/projects/notifications/api.py | 2 - taiga/users/serializers.py | 5 +- 7 files changed, 35 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index e07f042c..2fb3d0b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ before_install: - sudo /etc/init.d/postgresql stop - sudo apt-get install -y postgresql-9.4 - sudo apt-get install -y postgresql-plpython-9.4 + - sudo /etc/init.d/postgresql start - psql -c 'create database taiga;' -U postgres install: - travis_retry pip install -r requirements-devel.txt diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py index a7efeadf..bab90160 100644 --- a/taiga/base/api/fields.py +++ b/taiga/base/api/fields.py @@ -613,6 +613,8 @@ class ChoiceField(WritableField): def validate_user_email_allowed_domains(value): + validators.validate_email(value) + domain_name = value.split("@")[1] if settings.USER_EMAIL_ALLOWED_DOMAINS and domain_name not in settings.USER_EMAIL_ALLOWED_DOMAINS: @@ -627,7 +629,7 @@ class EmailField(CharField): default_error_messages = { "invalid": _("Enter a valid email address."), } - default_validators = [validators.validate_email, validate_user_email_allowed_domains] + default_validators = [validate_user_email_allowed_domains] def from_native(self, value): ret = super(EmailField, self).from_native(value) diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index 986c8f19..f2a023c5 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -65,6 +65,9 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, obj.size = obj.attached_file.size obj.name = path.basename(obj.attached_file.name) + if obj.content_object is None: + raise exc.WrongArguments(_("Object id issue isn't exists")) + if obj.project_id != obj.content_object.project_id: raise exc.WrongArguments(_("Project ID not matches between object and project")) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 23bbf598..f474de86 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import random import datetime from os import path from hashlib import sha1 @@ -33,6 +32,7 @@ from sampledatahelper.helper import SampleDataHelper from taiga.users.models import * from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.choices import BLOCKED_BY_STAFF +from taiga.external_apps.models import Application, ApplicationToken from taiga.projects.models import * from taiga.projects.milestones.models import * from taiga.projects.notifications.choices import NotifyLevel @@ -115,10 +115,12 @@ NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4)) NUM_USS_BACK = getattr(settings, "SAMPLE_DATA_NUM_USS_BACK", (8, 20)) NUM_ISSUES = getattr(settings, "SAMPLE_DATA_NUM_ISSUES", (12, 25)) NUM_WIKI_LINKS = getattr(settings, "SAMPLE_DATA_NUM_WIKI_LINKS", (0, 15)) -NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (0, 4)) +NUM_ATTACHMENTS = getattr(settings, "SAMPLE_DATA_NUM_ATTACHMENTS", (1, 4)) NUM_LIKES = getattr(settings, "SAMPLE_DATA_NUM_LIKES", (0, 10)) NUM_VOTES = getattr(settings, "SAMPLE_DATA_NUM_VOTES", (0, 10)) NUM_WATCHERS = getattr(settings, "SAMPLE_DATA_NUM_PROJECT_WATCHERS", (0, 8)) +NUM_APPLICATIONS = getattr(settings, "SAMPLE_DATA_NUM_APPLICATIONS", (1, 3)) +NUM_APPLICATIONS_TOKENS = getattr(settings, "SAMPLE_DATA_NUM_APPLICATIONS_TOKENS", (1, 3)) FEATURED_PROJECTS_POSITIONS = [0, 1, 2] LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS = [0, 1, 2] @@ -181,36 +183,33 @@ class Command(BaseCommand): project=project, role=role, is_admin=self.sd.boolean(), - token=''.join(random.sample('abcdef0123456789', 10))) + token=self.sd.hex_chars(10,10)) if role.computable: computable_project_roles.add(role) # added custom attributes - if self.sd.boolean: - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - UserStoryCustomAttribute.objects.create(name=name, - description=self.sd.words(3, 12), - type=self.sd.choice(TYPES_CHOICES)[0], - project=project, - order=i) - if self.sd.boolean: - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - TaskCustomAttribute.objects.create(name=name, - description=self.sd.words(3, 12), - type=self.sd.choice(TYPES_CHOICES)[0], - project=project, - order=i) - if self.sd.boolean: - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - IssueCustomAttribute.objects.create(name=name, + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + UserStoryCustomAttribute.objects.create(name=name, description=self.sd.words(3, 12), type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + TaskCustomAttribute.objects.create(name=name, + description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], + project=project, + order=i) + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + IssueCustomAttribute.objects.create(name=name, + description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], + project=project, + order=i) # If the project isn't empty if x not in empty_projects_range: @@ -272,6 +271,7 @@ class Command(BaseCommand): self.create_likes(project) + def create_attachment(self, obj, order): attached_file = self.sd.file_from_directory(*ATTACHMENT_SAMPLE_DATA) membership = self.sd.db_object_from_queryset(obj.project.memberships @@ -503,7 +503,7 @@ class Command(BaseCommand): project = Project.objects.create(slug='project-%s'%(counter), name='Project Example {0}'.format(counter), description='Project example {0} description'.format(counter), - owner=random.choice(self.users), + owner=self.sd.choice(self.users), is_private=is_private, anon_permissions=anon_permissions, public_permissions=public_permissions, @@ -533,7 +533,7 @@ class Command(BaseCommand): user = User.objects.create(username=username, full_name=full_name, email=email, - token=''.join(random.sample('abcdef0123456789', 10)), + token=self.sd.hex_chars(10,10), color=self.sd.choice(COLOR_CHOICES)) user.set_password('123123') diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 2c0f4027..7b3c218d 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -180,6 +180,7 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): creation_template = models.ForeignKey("projects.ProjectTemplate", related_name="projects", null=True, + on_delete=models.SET_NULL, blank=True, default=None, verbose_name=_("creation template")) diff --git a/taiga/projects/notifications/api.py b/taiga/projects/notifications/api.py index cd8f564e..9936ae52 100644 --- a/taiga/projects/notifications/api.py +++ b/taiga/projects/notifications/api.py @@ -21,9 +21,7 @@ from django.db.models import Q from taiga.base.api import ModelCrudViewSet from taiga.projects.notifications.choices import NotifyLevel -from taiga.projects.notifications.models import Watched from taiga.projects.models import Project -from taiga.users import services as user_services from . import serializers from . import models from . import permissions diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index e2e55ea9..a720e46e 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -24,10 +24,9 @@ from taiga.base.fields import Field, MethodField, I18NField from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project -from .services import get_user_photo_url, get_big_photo_url, get_user_big_photo_url +from .services import get_user_photo_url, get_user_big_photo_url from taiga.users.gravatar import get_user_gravatar_id from taiga.users.models import User -from collections import namedtuple ###################################################### @@ -142,7 +141,7 @@ class RoleSerializer(serializers.LightSerializer): project = Field(attr="project_id") order = Field() computable = Field() - permissions = Field() + permissions = Field() members_count = MethodField() def get_members_count(self, obj): From 0e125d73429803835c80589f0310d246edfc9731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Aug 2016 10:11:21 +0200 Subject: [PATCH 140/261] Fix sample data to generate alwais the same data --- taiga/projects/management/commands/sample_data.py | 6 +++--- taiga/users/filters.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index f474de86..7ced0599 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -347,7 +347,7 @@ class Command(BaseCommand): bug.save() custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca - in project.issuecustomattributes.all() if self.sd.boolean()} + in project.issuecustomattributes.all().order_by('id') if self.sd.boolean()} if custom_attributes_values: bug.custom_attributes_values.attributes_values = custom_attributes_values bug.custom_attributes_values.save() @@ -399,7 +399,7 @@ class Command(BaseCommand): task.save() custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca - in project.taskcustomattributes.all() if self.sd.boolean()} + in project.taskcustomattributes.all().order_by('id') if self.sd.boolean()} if custom_attributes_values: task.custom_attributes_values.attributes_values = custom_attributes_values task.custom_attributes_values.save() @@ -447,7 +447,7 @@ class Command(BaseCommand): us.save() custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca - in project.userstorycustomattributes.all() if self.sd.boolean()} + in project.userstorycustomattributes.all().order_by('id') if self.sd.boolean()} if custom_attributes_values: us.custom_attributes_values.attributes_values = custom_attributes_values us.custom_attributes_values.save() diff --git a/taiga/users/filters.py b/taiga/users/filters.py index 4e4dc116..46dd88ac 100644 --- a/taiga/users/filters.py +++ b/taiga/users/filters.py @@ -16,11 +16,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.apps import apps - from taiga.base.filters import PermissionBasedFilterBackend from . import services + class ContactsFilterBackend(PermissionBasedFilterBackend): def filter_queryset(self, user, request, queryset, view): qs = queryset.filter(is_active=True) From f80c4cf90dde041119b948c217f43230d8eff21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Thu, 11 Aug 2016 07:54:02 +0200 Subject: [PATCH 141/261] Bug#4493: Return id on points/priority/severity... --- taiga/projects/serializers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index ecdae019..f50c0daf 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -38,6 +38,7 @@ from .notifications.choices import NotifyLevel ###################################################### class PointsSerializer(serializers.LightSerializer): + id = Field() name = I18NField() order = Field() value = Field() @@ -45,6 +46,7 @@ class PointsSerializer(serializers.LightSerializer): class UserStoryStatusSerializer(serializers.LightSerializer): + id = Field() name = I18NField() slug = Field() order = Field() @@ -56,6 +58,7 @@ class UserStoryStatusSerializer(serializers.LightSerializer): class TaskStatusSerializer(serializers.LightSerializer): + id = Field() name = I18NField() slug = Field() order = Field() @@ -65,6 +68,7 @@ class TaskStatusSerializer(serializers.LightSerializer): class SeveritySerializer(serializers.LightSerializer): + id = Field() name = I18NField() order = Field() color = Field() @@ -72,6 +76,7 @@ class SeveritySerializer(serializers.LightSerializer): class PrioritySerializer(serializers.LightSerializer): + id = Field() name = I18NField() order = Field() color = Field() @@ -79,6 +84,7 @@ class PrioritySerializer(serializers.LightSerializer): class IssueStatusSerializer(serializers.LightSerializer): + id = Field() name = I18NField() slug = Field() order = Field() @@ -88,6 +94,7 @@ class IssueStatusSerializer(serializers.LightSerializer): class IssueTypeSerializer(serializers.LightSerializer): + id = Field() name = I18NField() order = Field() color = Field() From 3c2e094e8f06cf5f22a4915157d798348435cf4d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 3 Aug 2016 13:19:38 +0200 Subject: [PATCH 142/261] Refactoring-tags-and-colors-in-API --- taiga/projects/issues/serializers.py | 5 ++-- taiga/projects/tagging/serializers.py | 31 ++++++++++++++++++++++ taiga/projects/tagging/validators.py | 19 ++++++------- taiga/projects/tasks/serializers.py | 4 +-- taiga/projects/userstories/serializers.py | 3 ++- tests/integration/test_issues_tags.py | 13 ++++----- tests/integration/test_tasks_tags.py | 17 ++++++------ tests/integration/test_userstories_tags.py | 17 ++++++------ 8 files changed, 73 insertions(+), 36 deletions(-) create mode 100644 taiga/projects/tagging/serializers.py diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 2b773b81..a76fbf7d 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -25,12 +25,14 @@ from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import WatchedResourceSerializer +from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, - StatusExtraInfoSerializerMixin, serializers.LightSerializer): + StatusExtraInfoSerializerMixin, + TaggedInProjectResourceSerializer, serializers.LightSerializer): id = Field() ref = Field() severity = Field(attr="severity_id") @@ -45,7 +47,6 @@ class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer external_reference = Field() version = Field() watchers = Field() - tags = Field() is_closed = Field() diff --git a/taiga/projects/tagging/serializers.py b/taiga/projects/tagging/serializers.py new file mode 100644 index 00000000..494b508a --- /dev/null +++ b/taiga/projects/tagging/serializers.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import serializers +from taiga.base.fields import MethodField + + +class TaggedInProjectResourceSerializer(serializers.LightSerializer): + tags = MethodField() + + def get_tags(self, obj): + if not obj.tags: + return [] + + project_tag_colors = dict(obj.project.tags_colors) + return [[tag, project_tag_colors.get(tag, None)] for tag in obj.tags] diff --git a/taiga/projects/tagging/validators.py b/taiga/projects/tagging/validators.py index 595a5a3f..90dd98cb 100644 --- a/taiga/projects/tagging/validators.py +++ b/taiga/projects/tagging/validators.py @@ -20,6 +20,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.api import validators +from taiga.base.exceptions import ValidationError from . import services from . import fields @@ -43,14 +44,14 @@ class CreateTagValidator(ProjectTagValidator): def validate_tag(self, attrs, source): tag = attrs.get(source, None) if services.tag_exist_for_project_elements(self.project, tag): - raise validators.ValidationError(_("The tag exists.")) + raise ValidationError(_("The tag exists.")) return attrs def validate_color(self, attrs, source): color = attrs.get(source, None) - if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): - raise validators.ValidationError(_("The color is not a valid HEX color.")) + if color is not None and not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + raise ValidationError(_("The color is not a valid HEX color.")) return attrs @@ -63,21 +64,21 @@ class EditTagTagValidator(ProjectTagValidator): def validate_from_tag(self, attrs, source): tag = attrs.get(source, None) if not services.tag_exist_for_project_elements(self.project, tag): - raise validators.ValidationError(_("The tag doesn't exist.")) + raise ValidationError(_("The tag doesn't exist.")) return attrs def validate_to_tag(self, attrs, source): tag = attrs.get(source, None) if services.tag_exist_for_project_elements(self.project, tag): - raise validators.ValidationError(_("The tag exists yet")) + raise ValidationError(_("The tag exists yet")) return attrs def validate_color(self, attrs, source): color = attrs.get(source, None) if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): - raise validators.ValidationError(_("The color is not a valid HEX color.")) + raise ValidationError(_("The color is not a valid HEX color.")) return attrs @@ -88,7 +89,7 @@ class DeleteTagValidator(ProjectTagValidator): def validate_tag(self, attrs, source): tag = attrs.get(source, None) if not services.tag_exist_for_project_elements(self.project, tag): - raise validators.ValidationError(_("The tag doesn't exist.")) + raise ValidationError(_("The tag doesn't exist.")) return attrs @@ -101,13 +102,13 @@ class MixTagsValidator(ProjectTagValidator): tags = attrs.get(source, None) for tag in tags: if not services.tag_exist_for_project_elements(self.project, tag): - raise validators.ValidationError(_("The tag doesn't exist.")) + raise ValidationError(_("The tag doesn't exist.")) return attrs def validate_to_tag(self, attrs, source): tag = attrs.get(source, None) if not services.tag_exist_for_project_elements(self.project, tag): - raise validators.ValidationError(_("The tag doesn't exist.")) + raise ValidationError(_("The tag doesn't exist.")) return attrs diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index cd649424..b7232cf8 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -26,13 +26,14 @@ from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import WatchedResourceSerializer +from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, - serializers.LightSerializer): + TaggedInProjectResourceSerializer, serializers.LightSerializer): id = Field() user_story = Field(attr="user_story_id") @@ -52,7 +53,6 @@ class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, watchers = Field() is_blocked = Field() blocked_note = Field() - tags = Field() is_closed = MethodField() def get_milestone_slug(self, obj): diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index b23d5708..abb10592 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -22,6 +22,7 @@ from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin +from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin @@ -45,6 +46,7 @@ class UserStoryListSerializer( VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, + TaggedInProjectResourceSerializer, serializers.LightSerializer): id = Field() @@ -71,7 +73,6 @@ class UserStoryListSerializer( watchers = Field() is_blocked = Field() blocked_note = Field() - tags = Field() total_points = MethodField() comment = MethodField() origin_issue = OriginIssueSerializer(attr="generated_from_issue") diff --git a/tests/integration/test_issues_tags.py b/tests/integration/test_issues_tags.py index c86dc7aa..cb355f8f 100644 --- a/tests/integration/test_issues_tags.py +++ b/tests/integration/test_issues_tags.py @@ -123,7 +123,7 @@ def test_api_issue_add_new_tags_with_colors(client): def test_api_create_new_issue_with_tags(client): - project = f.ProjectFactory.create() + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) status = f.IssueStatusFactory.create(project=project) project.default_issue_status = status project.save() @@ -145,16 +145,17 @@ def test_api_create_new_issue_with_tags(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.data - assert ("back" in response.data["tags"] and - "front" in response.data["tags"] and - "ux" in response.data["tags"]) + issue_tags_colors = OrderedDict(response.data["tags"]) + + assert issue_tags_colors["back"] == "#fff8e7" + assert issue_tags_colors["front"] == "#aaaaaa" + assert issue_tags_colors["ux"] == "#fabada" tags_colors = OrderedDict(project.tags_colors) - assert not tags_colors.keys() project.refresh_from_db() tags_colors = OrderedDict(project.tags_colors) - assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors assert tags_colors["back"] == "#fff8e7" assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_tasks_tags.py b/tests/integration/test_tasks_tags.py index 08bf434d..60856688 100644 --- a/tests/integration/test_tasks_tags.py +++ b/tests/integration/test_tasks_tags.py @@ -123,7 +123,7 @@ def test_api_task_add_new_tags_with_colors(client): def test_api_create_new_task_with_tags(client): - project = f.ProjectFactory.create() + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) status = f.TaskStatusFactory.create(project=project) project.default_task_status = status project.save() @@ -135,8 +135,8 @@ def test_api_create_new_task_with_tags(client): "project": project.id, "tags": [ ["back", "#fff8e7"], - ["front", None], - ["ux", "#fabada"] + ["front", "#bbbbbb"], + ["ux", None] ] } @@ -145,16 +145,17 @@ def test_api_create_new_task_with_tags(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.data - assert ("back" in response.data["tags"] and - "front" in response.data["tags"] and - "ux" in response.data["tags"]) + task_tags_colors = OrderedDict(response.data["tags"]) + + assert task_tags_colors["back"] == "#fff8e7" + assert task_tags_colors["front"] == "#aaaaaa" + assert task_tags_colors["ux"] == "#fabada" tags_colors = OrderedDict(project.tags_colors) - assert not tags_colors.keys() project.refresh_from_db() tags_colors = OrderedDict(project.tags_colors) - assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors assert tags_colors["back"] == "#fff8e7" assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_userstories_tags.py b/tests/integration/test_userstories_tags.py index c2244f19..d89c172f 100644 --- a/tests/integration/test_userstories_tags.py +++ b/tests/integration/test_userstories_tags.py @@ -123,7 +123,7 @@ def test_api_user_story_add_new_tags_with_colors(client): def test_api_create_new_user_story_with_tags(client): - project = f.ProjectFactory.create() + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) status = f.UserStoryStatusFactory.create(project=project) project.default_userstory_status = status project.save() @@ -135,8 +135,8 @@ def test_api_create_new_user_story_with_tags(client): "project": project.id, "tags": [ ["back", "#fff8e7"], - ["front", None], - ["ux", "#fabada"] + ["front", "#bbbbbb"], + ["ux", None] ] } @@ -145,16 +145,17 @@ def test_api_create_new_user_story_with_tags(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 201, response.data - assert ("back" in response.data["tags"] and - "front" in response.data["tags"] and - "ux" in response.data["tags"]) + us_tags_colors = OrderedDict(response.data["tags"]) + + assert us_tags_colors["back"] == "#fff8e7" + assert us_tags_colors["front"] == "#aaaaaa" + assert us_tags_colors["ux"] == "#fabada" tags_colors = OrderedDict(project.tags_colors) - assert not tags_colors.keys() project.refresh_from_db() tags_colors = OrderedDict(project.tags_colors) - assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors assert tags_colors["back"] == "#fff8e7" assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" From b2a55d84f11d557078e5c36acba05fb622d46f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Sat, 20 Aug 2016 23:47:27 +0200 Subject: [PATCH 143/261] Use postgresql 9.4 in TravisCI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2fb3d0b7..ac145daa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ before_install: - sudo /etc/init.d/postgresql stop - sudo apt-get install -y postgresql-9.4 - sudo apt-get install -y postgresql-plpython-9.4 - - sudo /etc/init.d/postgresql start + - sudo /etc/init.d/postgresql start 9.4 - psql -c 'create database taiga;' -U postgres install: - travis_retry pip install -r requirements-devel.txt From adf964d0dff67283cdde9bd777e74012d2decf5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 19 Aug 2016 10:46:32 +0200 Subject: [PATCH 144/261] Sorting tasks and attachments inside us and tasks correctly --- taiga/projects/attachments/utils.py | 3 ++- taiga/projects/userstories/utils.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/taiga/projects/attachments/utils.py b/taiga/projects/attachments/utils.py index 33e36c44..5103fccb 100644 --- a/taiga/projects/attachments/utils.py +++ b/taiga/projects/attachments/utils.py @@ -37,7 +37,8 @@ def attach_basic_attachments(queryset, as_field="attachments_attr"): attachments_attachment.id, attachments_attachment.attached_file FROM attachments_attachment - WHERE attachments_attachment.object_id = {tbl}.id AND attachments_attachment.content_type_id = {type_id}) t""" + WHERE attachments_attachment.object_id = {tbl}.id AND attachments_attachment.content_type_id = {type_id} + ORDER BY attachments_attachment.order, attachments_attachment.id) t""" sql = sql.format(tbl=model._meta.db_table, type_id=type.id) queryset = queryset.extra(select={as_field: sql}) diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index 87b8e094..35456b71 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -89,7 +89,10 @@ def attach_tasks(queryset, as_field="tasks_attr"): projects_taskstatus.is_closed FROM tasks_task INNER JOIN projects_taskstatus on projects_taskstatus.id = tasks_task.status_id - WHERE user_story_id = {tbl}.id) t""" + WHERE user_story_id = {tbl}.id + ORDER BY tasks_task.us_order, tasks_task.ref + ) t + """ sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) From 8ac8fd4a623977189baca32a6d58cbed544cd992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 22 Aug 2016 19:21:45 +0200 Subject: [PATCH 145/261] [i18n] Update locales --- taiga/locale/es/LC_MESSAGES/django.po | 2 +- taiga/locale/fi/LC_MESSAGES/django.po | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index 00614517..736b8087 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ # Translators: # David Barragán , 2015-2016 # Esther Moreno , 2015 -# gustavodiazjaimes , 2015 +# Gustavo Díaz Jaimes , 2015 # Hector Colina , 2015 # Jesus Marin , 2015 # Jorge Sanchez , 2016 diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po index 4034a724..e36fb68a 100644 --- a/taiga/locale/fi/LC_MESSAGES/django.po +++ b/taiga/locale/fi/LC_MESSAGES/django.po @@ -5,13 +5,14 @@ # Translators: # artol , 2015 # David Barragán , 2015 +# Sami Singh , 2016 msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" -"Last-Translator: Taiga Dev Team \n" +"PO-Revision-Date: 2016-08-14 23:29+0000\n" +"Last-Translator: Sami Singh \n" "Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fi/)\n" "MIME-Version: 1.0\n" @@ -40,7 +41,7 @@ msgstr "tuntematon käyttäjänimi" msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" -"Vaaditaan. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'" +"Pakollinen. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'" #: taiga/auth/services.py:75 msgid "Username is already in use." @@ -60,7 +61,7 @@ msgstr "Käyttäjä on jo rekisteröitynyt." #: taiga/auth/services.py:146 msgid "This user is already a member of the project." -msgstr "" +msgstr "Tämä käyttäjä on jo projektin jäsen." #: taiga/auth/services.py:172 msgid "Error on creating new user." @@ -192,7 +193,7 @@ msgstr "" #: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 #: taiga/webhooks/api.py:68 msgid "Blocked element" -msgstr "" +msgstr "Estetty elementti" #: taiga/base/api/pagination.py:213 msgid "Page is not 'last', nor can it be converted to an int." @@ -344,7 +345,7 @@ msgstr "Precondition error" #: taiga/base/exceptions.py:217 msgid "No room left for more projects." -msgstr "" +msgstr "Ei enää tilaa uusille projekteille." #: taiga/base/filters.py:79 taiga/base/filters.py:444 msgid "Error in filter params types." @@ -482,6 +483,9 @@ msgid "" "%(comment)s

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

kommentti:

\n" +"

%(comment)s

" #: taiga/base/templates/emails/updates-body-text.jinja:6 #, python-format @@ -495,7 +499,7 @@ msgstr "" #: taiga/export_import/api.py:119 msgid "We needed at least one role" -msgstr "" +msgstr "Tarvitsemme ainakin yhden roolin" #: taiga/export_import/api.py:309 msgid "Needed dump file" @@ -583,7 +587,7 @@ msgstr "virhe aikajanojen tuonnissa" #: taiga/export_import/services/store.py:731 msgid "unexpected error importing project" -msgstr "" +msgstr "odottamaton virhe projektia tuotaessa" #: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 msgid "Error generating project dump" From 534445b30c26cc8ac853e0ac370d64f3105dc91e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 23 Aug 2016 13:26:58 +0200 Subject: [PATCH 146/261] Fix some error messages --- taiga/projects/tagging/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/tagging/validators.py b/taiga/projects/tagging/validators.py index 90dd98cb..4de1f395 100644 --- a/taiga/projects/tagging/validators.py +++ b/taiga/projects/tagging/validators.py @@ -44,7 +44,7 @@ class CreateTagValidator(ProjectTagValidator): def validate_tag(self, attrs, source): tag = attrs.get(source, None) if services.tag_exist_for_project_elements(self.project, tag): - raise ValidationError(_("The tag exists.")) + raise ValidationError(_("This tag already exists.")) return attrs @@ -71,7 +71,7 @@ class EditTagTagValidator(ProjectTagValidator): def validate_to_tag(self, attrs, source): tag = attrs.get(source, None) if services.tag_exist_for_project_elements(self.project, tag): - raise ValidationError(_("The tag exists yet")) + raise ValidationError(_("This tag already exists.")) return attrs From 54baf7b4dcaac5f432515e3ce9357293860c0fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 23 Aug 2016 13:58:58 +0200 Subject: [PATCH 147/261] Allow to edit tags and set no color --- taiga/projects/tagging/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/tagging/validators.py b/taiga/projects/tagging/validators.py index 4de1f395..779c247c 100644 --- a/taiga/projects/tagging/validators.py +++ b/taiga/projects/tagging/validators.py @@ -77,7 +77,7 @@ class EditTagTagValidator(ProjectTagValidator): def validate_color(self, attrs, source): color = attrs.get(source, None) - if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): + if color and not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): raise ValidationError(_("The color is not a valid HEX color.")) return attrs From 6134d7303d73bdef614223c8364965130a7f6b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 24 Aug 2016 18:08:01 +0200 Subject: [PATCH 148/261] Fix a typo --- taiga/base/templates/emails/base-body-html.jinja | 2 +- taiga/base/templates/emails/hero-body-html.jinja | 2 +- taiga/base/templates/emails/updates-body-html.jinja | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/taiga/base/templates/emails/base-body-html.jinja b/taiga/base/templates/emails/base-body-html.jinja index 57f331a5..ba857bb7 100644 --- a/taiga/base/templates/emails/base-body-html.jinja +++ b/taiga/base/templates/emails/base-body-html.jinja @@ -425,7 +425,7 @@
{{ support_url}}
Contact us: - + {{ support_email }}
diff --git a/taiga/base/templates/emails/hero-body-html.jinja b/taiga/base/templates/emails/hero-body-html.jinja index c88c7e5f..2f7d720e 100644 --- a/taiga/base/templates/emails/hero-body-html.jinja +++ b/taiga/base/templates/emails/hero-body-html.jinja @@ -399,7 +399,7 @@ {{ support_url}}
Contact us: - + {{ support_email }}
diff --git a/taiga/base/templates/emails/updates-body-html.jinja b/taiga/base/templates/emails/updates-body-html.jinja index af69858e..94d5b1ff 100644 --- a/taiga/base/templates/emails/updates-body-html.jinja +++ b/taiga/base/templates/emails/updates-body-html.jinja @@ -461,7 +461,7 @@ {{ support_url}}
Contact us: - + {{ support_email }}
From 9a2d88b9400d629922931d9237a2cbdb77efb633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 25 Aug 2016 12:31:27 +0200 Subject: [PATCH 149/261] Fix new-comment-permission migration --- taiga/users/migrations/0020_auto_20160525_1229.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/taiga/users/migrations/0020_auto_20160525_1229.py b/taiga/users/migrations/0020_auto_20160525_1229.py index 765eb3e1..5e73a57e 100644 --- a/taiga/users/migrations/0020_auto_20160525_1229.py +++ b/taiga/users/migrations/0020_auto_20160525_1229.py @@ -40,9 +40,9 @@ class Migration(migrations.Migration): comment_permission="comment_issue") ), - # issues + # wiki pages migrations.RunSQL(UPDATE_ROLES_PERMISSIONS_SQL.format( - base_permission="modify_issue", - comment_permission="comment_issue") + base_permission="modify_wiki_page", + comment_permission="comment_wiki_page") ) ] From 2df813aeb95dd1f499a33eebe1f96847795ce7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 29 Aug 2016 12:35:31 +0200 Subject: [PATCH 150/261] Fix error when delete projects directly in the admin panel list --- taiga/projects/admin.py | 34 +++++++++++++++++++++++++++++++++- taiga/projects/models.py | 1 + 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 94bfdc42..344f2344 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -176,8 +176,10 @@ class ProjectAdmin(admin.ModelAdmin): ## Actions actions = [ "make_public", - "make_private" + "make_private", + "delete_selected" ] + @transaction.atomic def make_public(self, request, queryset): total_updates = 0 @@ -210,6 +212,36 @@ class ProjectAdmin(admin.ModelAdmin): self.message_user(request, _("{count} successfully made private.").format(count=total_updates)) make_private.short_description = _("Make private") + def delete_selected(self, request, queryset): + # NOTE: This must be equal to taiga.projects.models.Project.delete_related_content + from taiga.events.apps import (connect_events_signals, + disconnect_events_signals) + from taiga.projects.tasks.apps import (connect_all_tasks_signals, + disconnect_all_tasks_signals) + from taiga.projects.userstories.apps import (connect_all_userstories_signals, + disconnect_all_userstories_signals) + from taiga.projects.issues.apps import (connect_all_issues_signals, + disconnect_all_issues_signals) + from taiga.projects.apps import (connect_memberships_signals, + disconnect_memberships_signals) + + disconnect_events_signals() + disconnect_all_issues_signals() + disconnect_all_tasks_signals() + disconnect_all_userstories_signals() + disconnect_memberships_signals() + + r = admin.actions.delete_selected(self, request, queryset) + + connect_events_signals() + connect_all_issues_signals() + connect_all_tasks_signals() + connect_all_userstories_signals() + connect_memberships_signals() + + return r + delete_selected.short_description = _("Delete selected %(verbose_name_plural)s") + # User Stories common admins class PointsAdmin(admin.ModelAdmin): diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 7b3c218d..6f9b386c 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -461,6 +461,7 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): set_notify_policy_level_to_ignore(notify_policy) def delete_related_content(self): + # NOTE: Remember to update code in taiga.projects.admin.ProjectAdmin.delete_selected from taiga.events.apps import (connect_events_signals, disconnect_events_signals) from taiga.projects.tasks.apps import (connect_all_tasks_signals, From b7d8dbc1a776fa16137a230407b5634a545cb93f Mon Sep 17 00:00:00 2001 From: Michael Jurke Date: Thu, 1 Sep 2016 23:34:37 +0200 Subject: [PATCH 151/261] Adds created-, modified-, finished- and finish_date to base filters Extends issues api filters with created_date, modified_date and finished_date --- taiga/base/filters.py | 64 ++++++++++++++++++++ taiga/projects/issues/api.py | 5 +- tests/integration/test_issues.py | 101 +++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index e70b8390..d8010e31 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -18,6 +18,8 @@ import logging +from dateutil.parser import parse as parse_date + from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.db.models import Q @@ -447,6 +449,68 @@ class WatchersFilter(FilterBackend): return super().filter_queryset(request, queryset, view) +class BaseCompareFilter(FilterBackend): + operators = ["", "lt", "gt", "lte", "gte"] + + def __init__(self, filter_name_base=None, operators=None): + if filter_name_base: + self.filter_name_base = filter_name_base + + def _get_filter_names(self): + return [ + self._get_filter_name(operator) + for operator in self.operators + ] + + def _get_filter_name(self, operator): + if operator and len(operator) > 0: + return "{base}__{operator}".format( + base=self.filter_name_base, operator=operator + ) + else: + return self.filter_name_base + + def _get_constraints(self, params): + constraints = {} + for filter_name in self._get_filter_names(): + raw_value = params.get(filter_name, None) + if raw_value is not None: + constraints[filter_name] = self._get_value(raw_value) + return constraints + + def _get_value(self, raw_value): + return raw_value + + def filter_queryset(self, request, queryset, view): + constraints = self._get_constraints(request.QUERY_PARAMS) + + if len(constraints) > 0: + queryset = queryset.filter(**constraints) + + return super().filter_queryset(request, queryset, view) + + +class BaseDateFilter(BaseCompareFilter): + def _get_value(self, raw_value): + return parse_date(raw_value) + + +class CreatedDateFilter(BaseDateFilter): + filter_name_base = "created_date" + + +class ModifiedDateFilter(BaseDateFilter): + filter_name_base = "modified_date" + + +class FinishedDateFilter(BaseDateFilter): + filter_name_base = "finished_date" + + +class FinishDateFilter(BaseDateFilter): + filter_name_base = "finish_date" + + ##################################################################### # Text search filters ##################################################################### diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 093b3ad1..08533e24 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -58,7 +58,10 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W filters.TagsFilter, filters.WatchersFilter, filters.QFilter, - filters.OrderByFilterMixin) + filters.OrderByFilterMixin, + filters.CreatedDateFilter, + filters.ModifiedDateFilter, + filters.FinishedDateFilter) retrieve_exclude_filters = (filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 5a0cc00c..a42715cd 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -19,6 +19,10 @@ import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock @@ -221,6 +225,103 @@ def test_api_filter_by_text_6(client): assert number_of_issues == 1 +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_created_date__gt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__gt=%s" % ( + quote(one_day_ago.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_created_date__gte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__gte=%s" % ( + quote(one_day_ago.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 2 + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__lt=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["ref"] == old_issue.ref + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_issue = f.create_issue(owner=user, created_date=one_day_ago) + issue = f.create_issue(owner=user) + + url = reverse("issues-list") + "?created_date__lte=%s" % ( + quote(issue.created_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 2 + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) From cc055f26346ab7d007375f58e4dd400b372dbaa9 Mon Sep 17 00:00:00 2001 From: Michael Jurke Date: Thu, 1 Sep 2016 23:35:37 +0200 Subject: [PATCH 152/261] Update AUTHORS and CHANGELOG --- AUTHORS.rst | 1 + CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index c7aff636..0f61b4da 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -29,6 +29,7 @@ answer newbie questions, and generally made taiga that much better: - Joe Letts - Julien Palard - luyikei +- Michael Jurke - Motius GmbH - Riccardo Coccioli - Ricky Posner diff --git a/CHANGELOG.md b/CHANGELOG.md index b2890f4e..9dcee9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ - Improve messages generated on webhooks input. - Add mentions support in commit messages. - Cleanup hooks code. +- Add created-, modified-, finished- and finish_date queryset filters + - Support exact match, gt, gte, lt, lte ### Misc - [API] Improve performance of some calls over list. From 0ec1da600c7b8fbbb5f9b06ab6d78a3798e8c608 Mon Sep 17 00:00:00 2001 From: Michael Jurke Date: Fri, 2 Sep 2016 01:30:45 +0200 Subject: [PATCH 153/261] Add tests for issues modified_date and finished_date --- tests/integration/test_issues.py | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index a42715cd..b2055bc4 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -322,6 +322,52 @@ def test_api_filter_by_created_date__lte(client): assert number_of_issues == 2 +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + _day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + older_issue = f.create_issue(owner=user) + issue = f.create_issue(owner=user) + # we have to refresh as it slightly differs + issue.refresh_from_db() + + assert older_issue.modified_date < issue.modified_date + + url = reverse("issues-list") + "?modified_date__gte=%s" % ( + quote(issue.modified_date.isoformat()) + ) + + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == issue.ref + + +def test_api_filter_by_finished_date(client): + user = f.UserFactory(is_superuser=True) + project = f.ProjectFactory.create() + status0 = f.IssueStatusFactory.create(project=project, is_closed=True) + + issue = f.create_issue(owner=user) + finished_issue = f.create_issue(owner=user, status=status0) + + assert finished_issue.finished_date + + url = reverse("issues-list") + "?finished_date__gte=%s" % ( + quote(finished_issue.finished_date.isoformat()) + ) + client.login(issue.owner) + response = client.get(url) + number_of_issues = len(response.data) + + assert response.status_code == 200 + assert number_of_issues == 1 + assert response.data[0]["ref"] == finished_issue.ref + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) From 87f81154ecf1316e5a4314b055634b62e630d4f6 Mon Sep 17 00:00:00 2001 From: Michael Jurke Date: Fri, 2 Sep 2016 10:50:47 +0200 Subject: [PATCH 154/261] Add created-, modified- and finished_date filters to tasks --- taiga/projects/tasks/api.py | 5 +- tests/integration/test_tasks.py | 108 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 232d496e..3cf43913 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -56,7 +56,10 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, filters.StatusesFilter, filters.TagsFilter, filters.WatchersFilter, - filters.QFilter) + filters.QFilter, + filters.CreatedDateFilter, + filters.ModifiedDateFilter, + filters.FinishedDateFilter) retrieve_exclude_filters = (filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index c13add00..c9fd4d01 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -19,6 +19,10 @@ import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock @@ -451,6 +455,110 @@ def test_get_tasks_including_attachments(client): assert len(response.data[0].get("attachments")) == 1 +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user, subject="test") + + url = reverse("tasks-list") + "?created_date=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == task.subject + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user, subject="test") + + url = reverse("tasks-list") + "?created_date__lt=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["subject"] == old_task.subject + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_task = f.create_task(owner=user, created_date=one_day_ago) + task = f.create_task(owner=user) + + url = reverse("tasks-list") + "?created_date__lte=%s" % ( + quote(task.created_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + _day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + older_task = f.create_task(owner=user) + task = f.create_task(owner=user, subject="test") + # we have to refresh as it slightly differs + task.refresh_from_db() + + assert older_task.modified_date < task.modified_date + + url = reverse("tasks-list") + "?modified_date__gte=%s" % ( + quote(task.modified_date.isoformat()) + ) + + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == task.subject + + +def test_api_filter_by_finished_date(client): + user = f.UserFactory(is_superuser=True) + project = f.ProjectFactory.create() + status0 = f.TaskStatusFactory.create(project=project, is_closed=True) + + task = f.create_task(owner=user) + finished_task = f.create_task(owner=user, status=status0, subject="test") + + assert finished_task.finished_date + + url = reverse("tasks-list") + "?finished_date__gte=%s" % ( + quote(finished_task.finished_date.isoformat()) + ) + client.login(task.owner) + response = client.get(url) + number_of_tasks = len(response.data) + + assert response.status_code == 200 + assert number_of_tasks == 1 + assert response.data[0]["subject"] == finished_task.subject + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) From dfdea74c588aef3995533e2f586aec00fa76cea1 Mon Sep 17 00:00:00 2001 From: Michael Jurke Date: Fri, 2 Sep 2016 11:02:04 +0200 Subject: [PATCH 155/261] Add created-, modified- and finish_date filters to userstories --- CHANGELOG.md | 1 + taiga/projects/userstories/api.py | 5 +- tests/integration/test_userstories.py | 110 ++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcee9c3..50dae19b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - Cleanup hooks code. - Add created-, modified-, finished- and finish_date queryset filters - Support exact match, gt, gte, lt, lte + - added issues, tasks and userstories accordingly ### Misc - [API] Improve performance of some calls over list. diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 61911d61..3b6e108a 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -64,7 +64,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi filters.TagsFilter, filters.WatchersFilter, filters.QFilter, - filters.OrderByFilterMixin) + filters.OrderByFilterMixin, + filters.CreatedDateFilter, + filters.ModifiedDateFilter, + filters.FinishDateFilter) retrieve_exclude_filters = (filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index b777b186..c9cd4bb1 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -19,6 +19,10 @@ import uuid import csv +import pytz + +from datetime import datetime, timedelta +from urllib.parse import quote from unittest import mock from django.core.urlresolvers import reverse @@ -501,6 +505,112 @@ def test_get_total_points(client): assert us_mixed.get_total_points() == 1.0 +def test_api_filter_by_created_date(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_userstory = f.create_userstory(owner=user, created_date=one_day_ago) + userstory = f.create_userstory(owner=user, subject="test") + + url = reverse("userstories-list") + "?created_date=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory.subject + + +def test_api_filter_by_created_date__lt(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_userstory = f.create_userstory( + owner=user, created_date=one_day_ago, subject="old test" + ) + userstory = f.create_userstory(owner=user) + + url = reverse("userstories-list") + "?created_date__lt=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert response.data[0]["subject"] == old_userstory.subject + + +def test_api_filter_by_created_date__lte(client): + user = f.UserFactory(is_superuser=True) + one_day_ago = datetime.now(pytz.utc) - timedelta(days=1) + + old_userstory = f.create_userstory(owner=user, created_date=one_day_ago) + userstory = f.create_userstory(owner=user) + + url = reverse("userstories-list") + "?created_date__lte=%s" % ( + quote(userstory.created_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 2 + + +def test_api_filter_by_modified_date__gte(client): + user = f.UserFactory(is_superuser=True) + + older_userstory = f.create_userstory(owner=user) + userstory = f.create_userstory(owner=user, subject="test") + # we have to refresh as it slightly differs + userstory.refresh_from_db() + + assert older_userstory.modified_date < userstory.modified_date + + url = reverse("userstories-list") + "?modified_date__gte=%s" % ( + quote(userstory.modified_date.isoformat()) + ) + + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory.subject + + +def test_api_filter_by_finish_date(client): + user = f.UserFactory(is_superuser=True) + one_day_later = datetime.now(pytz.utc) + timedelta(days=1) + + userstory = f.create_userstory(owner=user) + userstory_to_finish = f.create_userstory( + owner=user, finish_date=one_day_later, subject="test" + ) + + assert userstory_to_finish.finish_date + + url = reverse("userstories-list") + "?finish_date__gte=%s" % ( + quote(userstory_to_finish.finish_date.isoformat()) + ) + client.login(userstory.owner) + response = client.get(url) + number_of_userstories = len(response.data) + + assert response.status_code == 200 + assert number_of_userstories == 1 + assert response.data[0]["subject"] == userstory_to_finish.subject + + def test_api_filters_data(client): project = f.ProjectFactory.create() user1 = f.UserFactory.create(is_superuser=True) From 4a28721c733f80452c29f8b66bece095b74d5f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 8 Sep 2016 10:59:22 +0200 Subject: [PATCH 156/261] Disable Add action in Memberships admin panel to prevent mistakes --- taiga/projects/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index 344f2344..eecf7fef 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -35,6 +35,9 @@ class MembershipAdmin(admin.ModelAdmin): list_display_links = list_display raw_id_fields = ["project"] + def has_add_permission(self, request): + return False + def get_object(self, *args, **kwargs): self.obj = super().get_object(*args, **kwargs) return self.obj From 75a975113658e6f599055846e420d662637c6475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 24 Jun 2016 13:12:06 +0200 Subject: [PATCH 157/261] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50dae19b..af8bcd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.0.0 ??? (unreleased) ### Features +- Add Epics. - Include created, modified and finished dates for tasks in CSV reports. - Add gravatar url to Users API endpoint. - ProjectTemplates now are sorted by the attribute 'order'. From f70923c064d301118a79b9aa3d3091df6d0ac9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 29 Jun 2016 17:25:01 +0200 Subject: [PATCH 158/261] Initial epic model + epic status + epcic attachments + project template + project defaults --- settings/common.py | 1 + taiga/base/filters.py | 4 + taiga/permissions/choices.py | 8 + taiga/projects/api.py | 56 ++-- taiga/projects/attachments/api.py | 6 + taiga/projects/attachments/permissions.py | 13 +- taiga/projects/epics/__init__.py | 20 ++ taiga/projects/epics/apps.py | 51 ++++ .../projects/epics/migrations/0001_initial.py | 73 ++++++ taiga/projects/epics/migrations/__init__.py | 0 taiga/projects/epics/models.py | 91 +++++++ .../fixtures/initial_project_templates.json | 32 ++- .../management/commands/sample_data.py | 61 +++++ .../migrations/0030_auto_20151128_0757.py | 1 + .../0046_triggers_to_update_tags_colors.py | 3 + .../migrations/0049_auto_20160629_1443.py | 105 ++++++++ taiga/projects/models.py | 105 +++++--- taiga/projects/permissions.py | 12 + taiga/projects/serializers.py | 14 +- taiga/projects/services/__init__.py | 5 +- taiga/projects/services/bulk_update_order.py | 17 ++ taiga/projects/utils.py | 21 ++ taiga/projects/validators.py | 9 +- .../migrations/0022_auto_20160629_1443.py | 21 ++ tests/factories.py | 46 +++- .../test_projects_choices_resources.py | 240 +++++++++++++++++- 26 files changed, 931 insertions(+), 84 deletions(-) create mode 100644 taiga/projects/epics/__init__.py create mode 100644 taiga/projects/epics/apps.py create mode 100644 taiga/projects/epics/migrations/0001_initial.py create mode 100644 taiga/projects/epics/migrations/__init__.py create mode 100644 taiga/projects/epics/models.py create mode 100644 taiga/projects/migrations/0049_auto_20160629_1443.py create mode 100644 taiga/users/migrations/0022_auto_20160629_1443.py diff --git a/settings/common.py b/settings/common.py index 7f3f8cbd..7af70452 100644 --- a/settings/common.py +++ b/settings/common.py @@ -300,6 +300,7 @@ INSTALLED_APPS = [ "taiga.projects.likes", "taiga.projects.votes", "taiga.projects.milestones", + "taiga.projects.epics", "taiga.projects.userstories", "taiga.projects.tasks", "taiga.projects.issues", diff --git a/taiga/base/filters.py b/taiga/base/filters.py index d8010e31..bddec10e 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -198,6 +198,10 @@ class PermissionBasedAttachmentFilterBackend(PermissionBasedFilterBackend): return qs.filter(content_type=ct) +class CanViewEpicAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): + permission = "view_epic" + + class CanViewUserStoryAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): permission = "view_us" diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py index 48b92ace..5cda50a5 100644 --- a/taiga/permissions/choices.py +++ b/taiga/permissions/choices.py @@ -22,12 +22,14 @@ from django.utils.translation import ugettext_lazy as _ ANON_PERMISSIONS = [ ('view_project', _('View project')), ('view_milestones', _('View milestones')), + ('view_epic', _('View epic')), ('view_us', _('View user stories')), ('view_tasks', _('View tasks')), ('view_issues', _('View issues')), ('view_wiki_pages', _('View wiki pages')), ('view_wiki_links', _('View wiki links')), ] + MEMBERS_PERMISSIONS = [ ('view_project', _('View project')), # Milestone permissions @@ -36,6 +38,12 @@ MEMBERS_PERMISSIONS = [ ('modify_milestone', _('Modify milestone')), ('delete_milestone', _('Delete milestone')), # US permissions + ('view_epic', _('View epic')), + ('add_epic', _('Add epic')), + ('modify_epic', _('Modify epic')), + ('comment_epic', _('Comment epic')), + ('delete_epic', _('Delete epic')), + # US permissions ('view_us', _('View user story')), ('add_us', _('Add user story')), ('modify_us', _('Modify user story')), diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 89fdffe2..34920df4 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -40,6 +40,8 @@ from taiga.base.decorators import detail_route from taiga.base.utils.slug import slugify_uniquely from taiga.permissions import services as permissions_services + +from taiga.projects.epics.models import Epic from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.issues.models import Issue from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin @@ -49,7 +51,6 @@ from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.tasks.models import Task from taiga.projects.tagging.api import TagsColorsResourceMixin - from taiga.projects.userstories.models import UserStory, RolePoints from . import filters as project_filters @@ -110,17 +111,17 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, now = timezone.now() order_by_field_name = self._get_order_by_field_name() if order_by_field_name == "total_fans_last_week": - qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1)) + qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(weeks=1)) elif order_by_field_name == "total_fans_last_month": - qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1)) + qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(months=1)) elif order_by_field_name == "total_fans_last_year": - qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1)) + qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(years=1)) elif order_by_field_name == "total_activity_last_week": - qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(weeks=1)) + qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(weeks=1)) elif order_by_field_name == "total_activity_last_month": - qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(months=1)) + qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(months=1)) elif order_by_field_name == "total_activity_last_year": - qs = qs.filter(totals_updated_datetime__gte=now-relativedelta(years=1)) + qs = qs.filter(totals_updated_datetime__gte=now - relativedelta(years=1)) return qs @@ -449,21 +450,21 @@ class ProjectWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): ## Custom values for selectors ###################################################### -class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, - ModelCrudViewSet, BulkUpdateOrderMixin): +class EpicStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): - model = models.Points - serializer_class = serializers.PointsSerializer - validator_class = validators.PointsValidator - permission_classes = (permissions.PointsPermission,) + model = models.EpicStatus + serializer_class = serializers.EpicStatusSerializer + validator_class = validators.EpicStatusValidator + permission_classes = (permissions.EpicStatusPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) - bulk_update_param = "bulk_points" - bulk_update_perm = "change_points" - bulk_update_order_action = services.bulk_update_points_order - move_on_destroy_related_class = RolePoints - move_on_destroy_related_field = "points" - move_on_destroy_project_default_field = "default_points" + bulk_update_param = "bulk_epic_statuses" + bulk_update_perm = "change_epicstatus" + bulk_update_order_action = services.bulk_update_epic_status_order + move_on_destroy_related_class = Epic + move_on_destroy_related_field = "status" + move_on_destroy_project_default_field = "default_epic_status" class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, @@ -483,6 +484,23 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, move_on_destroy_project_default_field = "default_us_status" +class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, + ModelCrudViewSet, BulkUpdateOrderMixin): + + model = models.Points + serializer_class = serializers.PointsSerializer + validator_class = validators.PointsValidator + permission_classes = (permissions.PointsPermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ('project',) + bulk_update_param = "bulk_points" + bulk_update_perm = "change_points" + bulk_update_order_action = services.bulk_update_points_order + move_on_destroy_related_class = RolePoints + move_on_destroy_related_field = "points" + move_on_destroy_project_default_field = "default_points" + + class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin, ModelCrudViewSet, BulkUpdateOrderMixin): diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py index f2a023c5..3bcbf6cf 100644 --- a/taiga/projects/attachments/api.py +++ b/taiga/projects/attachments/api.py @@ -83,6 +83,12 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin, return obj.content_object +class EpicAttachmentViewSet(BaseAttachmentViewSet): + permission_classes = (permissions.EpicAttachmentPermission,) + filter_backends = (filters.CanViewEpicAttachmentFilterBackend,) + content_type = "epics.epic" + + class UserStoryAttachmentViewSet(BaseAttachmentViewSet): permission_classes = (permissions.UserStoryAttachmentPermission,) filter_backends = (filters.CanViewUserStoryAttachmentFilterBackend,) diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py index 4e0f5d3e..ee768014 100644 --- a/taiga/projects/attachments/permissions.py +++ b/taiga/projects/attachments/permissions.py @@ -28,6 +28,15 @@ class IsAttachmentOwnerPerm(PermissionComponent): return False +class EpicAttachmentPermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_epic') | IsAttachmentOwnerPerm() + create_perms = HasProjectPerm('modify_epic') + update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm() + partial_update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm() + destroy_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm() + list_perms = AllowAny() + + class UserStoryAttachmentPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_us') | IsAttachmentOwnerPerm() create_perms = HasProjectPerm('modify_us') @@ -67,7 +76,9 @@ class WikiAttachmentPermission(TaigaResourcePermission): class RawAttachmentPerm(PermissionComponent): def check_permissions(self, request, view, obj=None): is_owner = IsAttachmentOwnerPerm().check_permissions(request, view, obj) - if obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory": + if obj.content_type.app_label == "epics" and obj.content_type.model == "epic": + return EpicAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner + elif obj.content_type.app_label == "userstories" and obj.content_type.model == "userstory": return UserStoryAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner elif obj.content_type.app_label == "tasks" and obj.content_type.model == "task": return TaskAttachmentPermission(request, view).check_permissions('retrieve', obj) or is_owner diff --git a/taiga/projects/epics/__init__.py b/taiga/projects/epics/__init__.py new file mode 100644 index 00000000..cc0dd3b9 --- /dev/null +++ b/taiga/projects/epics/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +default_app_config = "taiga.projects.epics.apps.EpicsAppConfig" + diff --git a/taiga/projects/epics/apps.py b/taiga/projects/epics/apps.py new file mode 100644 index 00000000..cff4d699 --- /dev/null +++ b/taiga/projects/epics/apps.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.apps import AppConfig +from django.apps import apps +from django.db.models import signals + + +def connect_epics_signals(): + from taiga.projects.tagging import signals as tagging_handlers + + # Tags + signals.pre_save.connect(tagging_handlers.tags_normalization, + sender=apps.get_model("epics", "Epic"), + dispatch_uid="tags_normalization_epic") + + +def connect_all_epics_signals(): + connect_epics_signals() + + +def disconnect_epics_signals(): + signals.pre_save.disconnect(sender=apps.get_model("epics", "Task"), + dispatch_uid="tags_normalization") + + +def disconnect_all_epics_signals(): + disconnect_epics_signals() + + +class EpicsAppConfig(AppConfig): + name = "taiga.projects.epics" + verbose_name = "Epics" + + def ready(self): + connect_all_epics_signals() diff --git a/taiga/projects/epics/migrations/0001_initial.py b/taiga/projects/epics/migrations/0001_initial.py new file mode 100644 index 00000000..dc670d7e --- /dev/null +++ b/taiga/projects/epics/migrations/0001_initial.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-29 14:43 +from __future__ import unicode_literals + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import taiga.projects.notifications.mixins + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0049_auto_20160629_1443'), + ('userstories', '0012_auto_20160614_1201'), + ] + + operations = [ + migrations.CreateModel( + name='Epic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=[], null=True, size=None, verbose_name='tags')), + ('version', models.IntegerField(default=1, verbose_name='version')), + ('is_blocked', models.BooleanField(default=False, verbose_name='is blocked')), + ('blocked_note', models.TextField(blank=True, default='', verbose_name='blocked note')), + ('ref', models.BigIntegerField(blank=True, db_index=True, default=None, null=True, verbose_name='ref')), + ('is_closed', models.BooleanField(default=False)), + ('epics_order', models.IntegerField(default=10000, verbose_name='epics order')), + ('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')), + ('modified_date', models.DateTimeField(verbose_name='modified date')), + ('finish_date', models.DateTimeField(blank=True, null=True, verbose_name='finish date')), + ('subject', models.TextField(verbose_name='subject')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('client_requirement', models.BooleanField(default=False, verbose_name='is client requirement')), + ('team_requirement', models.BooleanField(default=False, verbose_name='is team requirement')), + ('assigned_to', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='epics_assigned_to_me', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_epics', to=settings.AUTH_USER_MODEL, verbose_name='owner')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epics', to='projects.Project', verbose_name='project')), + ('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='epics', to='projects.EpicStatus', verbose_name='status')), + ('user_stories', models.ManyToManyField(related_name='epics', to='userstories.UserStory', verbose_name='user stories')), + ], + options={ + 'verbose_name_plural': 'epics', + 'ordering': ['project', 'ref'], + 'verbose_name': 'epic', + }, + bases=(taiga.projects.notifications.mixins.WatchedModelMixin, models.Model), + ), + # Execute trigger after epic update + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_epic_update ON epics_epic; + CREATE TRIGGER update_project_tags_colors_on_epic_update + AFTER UPDATE ON epics_epic + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + # Execute trigger after epic insert + migrations.RunSQL( + """ + DROP TRIGGER IF EXISTS update_project_tags_colors_on_epic_insert ON epics_epic; + CREATE TRIGGER update_project_tags_colors_on_epic_insert + AFTER INSERT ON epics_epic + FOR EACH ROW EXECUTE PROCEDURE update_project_tags_colors(); + """ + ), + ] diff --git a/taiga/projects/epics/migrations/__init__.py b/taiga/projects/epics/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py new file mode 100644 index 00000000..b4a9118a --- /dev/null +++ b/taiga/projects/epics/models.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.db import models +from django.contrib.contenttypes.fields import GenericRelation +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone + +from taiga.projects.tagging.models import TaggedMixin +from taiga.projects.occ import OCCModelMixin +from taiga.projects.notifications.mixins import WatchedModelMixin +from taiga.projects.mixins.blocked import BlockedMixin + + +class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): + ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, + verbose_name=_("ref")) + project = models.ForeignKey("projects.Project", null=False, blank=False, + related_name="epics", verbose_name=_("project")) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, + related_name="owned_epics", verbose_name=_("owner"), + on_delete=models.SET_NULL) + status = models.ForeignKey("projects.EpicStatus", null=True, blank=True, + related_name="epics", verbose_name=_("status"), + on_delete=models.SET_NULL) + is_closed = models.BooleanField(default=False) + + epics_order = models.IntegerField(null=False, blank=False, default=10000, + verbose_name=_("epics order")) + + created_date = models.DateTimeField(null=False, blank=False, + verbose_name=_("created date"), + default=timezone.now) + modified_date = models.DateTimeField(null=False, blank=False, + verbose_name=_("modified date")) + finish_date = models.DateTimeField(null=True, blank=True, + verbose_name=_("finish date")) + + subject = models.TextField(null=False, blank=False, + verbose_name=_("subject")) + description = models.TextField(null=False, blank=True, verbose_name=_("description")) + assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, + default=None, related_name="epics_assigned_to_me", + verbose_name=_("assigned to")) + client_requirement = models.BooleanField(default=False, null=False, blank=True, + verbose_name=_("is client requirement")) + team_requirement = models.BooleanField(default=False, null=False, blank=True, + verbose_name=_("is team requirement")) + + user_stories = models.ManyToManyField("userstories.UserStory", related_name="epics", + verbose_name=_("user stories")) + + attachments = GenericRelation("attachments.Attachment") + + _importing = None + + class Meta: + verbose_name = "epic" + verbose_name_plural = "epics" + ordering = ["project", "ref"] + + def save(self, *args, **kwargs): + if not self._importing or not self.modified_date: + self.modified_date = timezone.now() + + if not self.status: + self.status = self.project.default_epic_status + + super().save(*args, **kwargs) + + def __str__(self): + return "({1}) {0}".format(self.ref, self.subject) + + def __repr__(self): + return "" % (self.id) diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index ea638da4..83d64b80 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -5,26 +5,28 @@ "fields": { "name": "Scrum", "slug": "scrum", - "order": 1, "description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers", + "order": 1, "created_date": "2014-04-22T14:48:43.596Z", - "modified_date": "2014-07-25T10:02:46.479Z", + "modified_date": "2016-06-29T14:52:11.273Z", "default_owner_role": "product-owner", + "is_epics_activated": true, "is_backlog_activated": true, "is_kanban_activated": false, "is_wiki_activated": true, "is_issues_activated": true, "videoconferences": null, "videoconferences_extra_data": "", - "default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}", - "us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#669900\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]", + "default_options": "{\"epic_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"us_status\": \"New\", \"points\": \"?\", \"priority\": \"Normal\", \"task_status\": \"New\", \"issue_status\": \"New\"}", + "epic_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"order\": 5, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}]", + "us_statuses": "[{\"name\": \"New\", \"is_archived\": false, \"wip_limit\": null, \"order\": 1, \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"name\": \"Ready\", \"is_archived\": false, \"wip_limit\": null, \"order\": 2, \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"name\": \"In progress\", \"is_archived\": false, \"wip_limit\": null, \"order\": 3, \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"name\": \"Ready for test\", \"is_archived\": false, \"wip_limit\": null, \"order\": 4, \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"name\": \"Done\", \"is_archived\": false, \"wip_limit\": null, \"order\": 5, \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}, {\"name\": \"Archived\", \"is_archived\": true, \"wip_limit\": null, \"order\": 6, \"color\": \"#5c3566\", \"slug\": \"archived\", \"is_closed\": true}]", "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]", - "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#ff9900\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#ffcc00\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#669900\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#999999\", \"is_closed\": false}]", - "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#8C2318\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#5E8C6A\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#88A65E\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#BFB35A\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#89BAB4\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#CC0000\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#666666\", \"is_closed\": false}]", + "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#ffcc00\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#669900\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#999999\", \"slug\": \"needs-info\", \"is_closed\": false}]", + "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#8C2318\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#5E8C6A\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#88A65E\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#BFB35A\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#89BAB4\", \"slug\": \"needs-info\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"color\": \"#CC0000\", \"slug\": \"rejected\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"color\": \"#666666\", \"slug\": \"posponed\", \"is_closed\": false}]", "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#89BAB4\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#ba89a8\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#89a8ba\"}]", "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#666666\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#669933\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]", "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#666666\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#669933\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#0000FF\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#FFA500\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]", - "roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]" + "roles": "[{\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false}]" } }, { @@ -33,26 +35,28 @@ "fields": { "name": "Kanban", "slug": "kanban", - "order": 2, "description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.", + "order": 2, "created_date": "2014-04-22T14:50:19.738Z", - "modified_date": "2014-07-25T13:11:42.754Z", + "modified_date": "2016-06-29T14:52:15.232Z", "default_owner_role": "product-owner", + "is_epics_activated": true, "is_backlog_activated": false, "is_kanban_activated": true, "is_wiki_activated": false, "is_issues_activated": false, "videoconferences": null, "videoconferences_extra_data": "", - "default_options": "{\"severity\": \"Normal\", \"priority\": \"Normal\", \"task_status\": \"New\", \"points\": \"?\", \"us_status\": \"New\", \"issue_type\": \"Bug\", \"issue_status\": \"New\"}", - "us_statuses": "[{\"is_archived\": false, \"slug\": \"new\", \"is_closed\": false, \"wip_limit\": null, \"order\": 1, \"name\": \"New\", \"color\": \"#999999\"}, {\"is_archived\": false, \"slug\": \"ready\", \"is_closed\": false, \"wip_limit\": null, \"order\": 2, \"name\": \"Ready\", \"color\": \"#f57900\"}, {\"is_archived\": false, \"slug\": \"in-progress\", \"is_closed\": false, \"wip_limit\": null, \"order\": 3, \"name\": \"In progress\", \"color\": \"#729fcf\"}, {\"is_archived\": false, \"slug\": \"ready-for-test\", \"is_closed\": false, \"wip_limit\": null, \"order\": 4, \"name\": \"Ready for test\", \"color\": \"#4e9a06\"}, {\"is_archived\": false, \"slug\": \"done\", \"is_closed\": true, \"wip_limit\": null, \"order\": 5, \"name\": \"Done\", \"color\": \"#cc0000\"}, {\"is_archived\": true, \"slug\": \"archived\", \"is_closed\": true, \"wip_limit\": null, \"order\": 6, \"name\": \"Archived\", \"color\": \"#5c3566\"}]", + "default_options": "{\"epic_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"us_status\": \"New\", \"points\": \"?\", \"priority\": \"Normal\", \"task_status\": \"New\", \"issue_status\": \"New\"}", + "epic_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"order\": 5, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}]", + "us_statuses": "[{\"name\": \"New\", \"is_archived\": false, \"wip_limit\": null, \"order\": 1, \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"name\": \"Ready\", \"is_archived\": false, \"wip_limit\": null, \"order\": 2, \"color\": \"#f57900\", \"slug\": \"ready\", \"is_closed\": false}, {\"name\": \"In progress\", \"is_archived\": false, \"wip_limit\": null, \"order\": 3, \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"name\": \"Ready for test\", \"is_archived\": false, \"wip_limit\": null, \"order\": 4, \"color\": \"#4e9a06\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"name\": \"Done\", \"is_archived\": false, \"wip_limit\": null, \"order\": 5, \"color\": \"#cc0000\", \"slug\": \"done\", \"is_closed\": true}, {\"name\": \"Archived\", \"is_archived\": true, \"wip_limit\": null, \"order\": 6, \"color\": \"#5c3566\", \"slug\": \"archived\", \"is_closed\": true}]", "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]", - "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}]", - "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"slug\": \"new\", \"color\": \"#999999\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"slug\": \"in-progress\", \"color\": \"#729fcf\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"color\": \"#f57900\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"slug\": \"closed\", \"color\": \"#4e9a06\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"slug\": \"needs-info\", \"color\": \"#cc0000\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"slug\": \"rejected\", \"color\": \"#d3d7cf\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"slug\": \"posponed\", \"color\": \"#75507b\", \"is_closed\": false}]", + "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"is_closed\": false}]", + "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"color\": \"#d3d7cf\", \"slug\": \"rejected\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"color\": \"#75507b\", \"slug\": \"posponed\", \"is_closed\": false}]", "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#cc0000\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#4e9a06\"}]", "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#999999\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]", "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#999999\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#f57900\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]", - "roles": "[{\"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}, {\"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false, \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"]}]" + "roles": "[{\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false}]" } } ] diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 7ced0599..b1726496 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -34,6 +34,7 @@ from taiga.permissions.choices import ANON_PERMISSIONS from taiga.projects.choices import BLOCKED_BY_STAFF from taiga.external_apps.models import Application, ApplicationToken from taiga.projects.models import * +from taiga.projects.epics.models import * from taiga.projects.milestones.models import * from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.services.stats import get_stats_for_project @@ -109,6 +110,8 @@ NUM_PROJECTS =getattr(settings, "SAMPLE_DATA_NUM_PROJECTS", 4) NUM_EMPTY_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_EMPTY_PROJECTS", 2) NUM_BLOCKED_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_BLOCKED_PROJECTS", 1) NUM_MILESTONES = getattr(settings, "SAMPLE_DATA_NUM_MILESTONES", (1, 5)) +NUM_EPICS = getattr(settings, "SAMPLE_DATA_NUM_EPICS", (4, 8)) +NUM_USS_EPICS = getattr(settings, "SAMPLE_DATA_NUM_USS_EPICS", (2, 6)) NUM_USS = getattr(settings, "SAMPLE_DATA_NUM_USS", (3, 7)) NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5)) NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4)) @@ -255,6 +258,11 @@ class Command(BaseCommand): if self.sd.boolean(): self.create_wiki_page(project, wiki_link.href) + # create epics + for y in range(self.sd.int(*NUM_EPICS)): + epic = self.create_epic(project) + + project.refresh_from_db() @@ -494,6 +502,59 @@ class Command(BaseCommand): return milestone + def create_epic(self, project): + epic = Epic.objects.create(subject=self.sd.choice(SUBJECT_CHOICES), + project=project, + owner=self.sd.db_object_from_queryset( + project.memberships.filter(user__isnull=False)).user, + description=self.sd.paragraph(), + status=self.sd.db_object_from_queryset(project.epic_statuses.filter( + is_closed=False)), + tags=self.sd.words(1, 3).split(" ")) + + # TODO: Epic custom attributes + #custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca + # in project.epiccustomattributes.all() if self.sd.boolean()} + #if custom_attributes_values: + # epic.custom_attributes_values.attributes_values = custom_attributes_values + # epic.custom_attributes_values.save() + + + for i in range(self.sd.int(*NUM_ATTACHMENTS)): + attachment = self.create_attachment(epic, i+1) + + if self.sd.choice([True, True, False, True, True]): + epic.assigned_to = self.sd.db_object_from_queryset(project.memberships.filter( + user__isnull=False)).user + epic.save() + + # TODO: Epic history + #take_snapshot(epic, + # comment=self.sd.paragraph(), + # user=epic.owner) + # + # Add history entry + #epic.status=self.sd.db_object_from_queryset(project.epic_statuses.filter(is_closed=False)) + #epic.save() + #take_snapshot(epic, + # comment=self.sd.paragraph(), + # user=epic.owner) + + # TODO: Epic voters + #self.create_votes(epic) + # TODO: Epic watchers + #self.create_watchers(epic) + + if self.sd.choice([True, True, False, True, True]): + filters = {} + if self.sd.choice([True, True, False, True, True]): + filters = {"project": epic.project} + n = self.sd.choice(list(range(self.sd.int(*NUM_USS_EPICS)))) + + epic.user_stories.add(*UserStory.objects.filter(**filters).order_by("?")[:n]) + + return epic + def create_project(self, counter, is_private=None, blocked_code=None): if is_private is None: is_private=self.sd.boolean() diff --git a/taiga/projects/migrations/0030_auto_20151128_0757.py b/taiga/projects/migrations/0030_auto_20151128_0757.py index 2a8631df..425598e7 100644 --- a/taiga/projects/migrations/0030_auto_20151128_0757.py +++ b/taiga/projects/migrations/0030_auto_20151128_0757.py @@ -110,6 +110,7 @@ class Migration(migrations.Migration): dependencies = [ ('projects', '0029_project_is_looking_for_people'), + ('likes', '0001_initial'), ('timeline', '0004_auto_20150603_1312'), ('likes', '0001_initial'), ] diff --git a/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py index abb4806d..af7fbf8d 100644 --- a/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py +++ b/taiga/projects/migrations/0046_triggers_to_update_tags_colors.py @@ -9,6 +9,9 @@ class Migration(migrations.Migration): dependencies = [ ('projects', '0045_merge'), + ('userstories', '0011_userstory_tribe_gig'), + ('tasks', '0009_auto_20151104_1131'), + ('issues', '0006_remove_issue_watchers'), ] operations = [ diff --git a/taiga/projects/migrations/0049_auto_20160629_1443.py b/taiga/projects/migrations/0049_auto_20160629_1443.py new file mode 100644 index 00000000..cdfd420b --- /dev/null +++ b/taiga/projects/migrations/0049_auto_20160629_1443.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-29 14:43 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import django_pgjson.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0048_auto_20160615_1508'), + ] + + operations = [ + migrations.CreateModel( + name='EpicStatus', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('slug', models.SlugField(blank=True, max_length=255, verbose_name='slug')), + ('order', models.IntegerField(default=10, verbose_name='order')), + ('is_closed', models.BooleanField(default=False, verbose_name='is closed')), + ('color', models.CharField(default='#999999', max_length=20, verbose_name='color')), + ], + options={ + 'verbose_name_plural': 'epic statuses', + 'ordering': ['project', 'order', 'name'], + 'verbose_name': 'epic status', + }, + ), + migrations.AlterModelOptions( + name='issuestatus', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'issue status', 'verbose_name_plural': 'issue statuses'}, + ), + migrations.AlterModelOptions( + name='issuetype', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'issue type', 'verbose_name_plural': 'issue types'}, + ), + migrations.AlterModelOptions( + name='membership', + options={'ordering': ['project', 'user__full_name', 'user__username', 'user__email', 'email'], 'verbose_name': 'membership', 'verbose_name_plural': 'memberships'}, + ), + migrations.AlterModelOptions( + name='points', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'points', 'verbose_name_plural': 'points'}, + ), + migrations.AlterModelOptions( + name='priority', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'priority', 'verbose_name_plural': 'priorities'}, + ), + migrations.AlterModelOptions( + name='severity', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'severity', 'verbose_name_plural': 'severities'}, + ), + migrations.AlterModelOptions( + name='taskstatus', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'task status', 'verbose_name_plural': 'task statuses'}, + ), + migrations.AlterModelOptions( + name='userstorystatus', + options={'ordering': ['project', 'order', 'name'], 'verbose_name': 'user story status', 'verbose_name_plural': 'user story statuses'}, + ), + migrations.AddField( + model_name='project', + name='is_epics_activated', + field=models.BooleanField(default=True, verbose_name='active epics panel'), + ), + migrations.AddField( + model_name='projecttemplate', + name='epic_statuses', + field=django_pgjson.fields.JsonField(blank=True, null=True, verbose_name='epic statuses'), + ), + migrations.AddField( + model_name='projecttemplate', + name='is_epics_activated', + field=models.BooleanField(default=True, verbose_name='active epics panel'), + ), + migrations.AlterField( + model_name='project', + name='anon_permissions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_epic', 'View epic'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'), + ), + migrations.AlterField( + model_name='project', + name='public_permissions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epic', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'), + ), + migrations.AddField( + model_name='epicstatus', + name='project', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epic_statuses', to='projects.Project', verbose_name='project'), + ), + migrations.AddField( + model_name='project', + name='default_epic_status', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='projects.EpicStatus', verbose_name='default epic status'), + ), + migrations.AlterUniqueTogether( + name='epicstatus', + unique_together=set([('project', 'slug'), ('project', 'name')]), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 6f9b386c..02d24519 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -102,19 +102,20 @@ class Membership(models.Model): verbose_name_plural = "memberships" unique_together = ("user", "project",) ordering = ["project", "user__full_name", "user__username", "user__email", "email"] - permissions = ( - ("view_membership", "Can view membership"), - ) class ProjectDefaults(models.Model): - default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL, - related_name="+", null=True, blank=True, - verbose_name=_("default points")) + default_epic_status = models.OneToOneField("projects.EpicStatus", + on_delete=models.SET_NULL, related_name="+", + null=True, blank=True, + verbose_name=_("default epic status")) default_us_status = models.OneToOneField("projects.UserStoryStatus", on_delete=models.SET_NULL, related_name="+", null=True, blank=True, verbose_name=_("default US status")) + default_points = models.OneToOneField("projects.Points", on_delete=models.SET_NULL, + related_name="+", null=True, blank=True, + verbose_name=_("default points")) default_task_status = models.OneToOneField("projects.TaskStatus", on_delete=models.SET_NULL, related_name="+", null=True, blank=True, @@ -164,6 +165,8 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): verbose_name=_("total of milestones")) total_story_points = models.FloatField(null=True, blank=True, verbose_name=_("total story points")) + is_epics_activated = models.BooleanField(default=True, null=False, blank=True, + verbose_name=_("active epics panel")) is_backlog_activated = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("active backlog panel")) is_kanban_activated = models.BooleanField(default=False, null=False, blank=True, @@ -504,6 +507,39 @@ class ProjectModulesConfig(models.Model): ordering = ["project"] +# Epic common Models +class EpicStatus(models.Model): + name = models.CharField(max_length=255, null=False, blank=False, + verbose_name=_("name")) + slug = models.SlugField(max_length=255, null=False, blank=True, + verbose_name=_("slug")) + order = models.IntegerField(default=10, null=False, blank=False, + verbose_name=_("order")) + is_closed = models.BooleanField(default=False, null=False, blank=True, + verbose_name=_("is closed")) + color = models.CharField(max_length=20, null=False, blank=False, default="#999999", + verbose_name=_("color")) + project = models.ForeignKey("Project", null=False, blank=False, + related_name="epic_statuses", verbose_name=_("project")) + + class Meta: + verbose_name = "epic status" + verbose_name_plural = "epic statuses" + ordering = ["project", "order", "name"] + unique_together = (("project", "name"), ("project", "slug")) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + qs = self.project.epic_statuses + if self.id: + qs = qs.exclude(id=self.id) + + self.slug = slugify_uniquely_for_queryset(self.name, qs) + return super().save(*args, **kwargs) + + # User Stories common Models class UserStoryStatus(models.Model): name = models.CharField(max_length=255, null=False, blank=False, @@ -528,9 +564,6 @@ class UserStoryStatus(models.Model): verbose_name_plural = "user story statuses" ordering = ["project", "order", "name"] unique_together = (("project", "name"), ("project", "slug")) - permissions = ( - ("view_userstorystatus", "Can view user story status"), - ) def __str__(self): return self.name @@ -559,9 +592,6 @@ class Points(models.Model): verbose_name_plural = "points" ordering = ["project", "order", "name"] unique_together = ("project", "name") - permissions = ( - ("view_points", "Can view points"), - ) def __str__(self): return self.name @@ -588,9 +618,6 @@ class TaskStatus(models.Model): verbose_name_plural = "task statuses" ordering = ["project", "order", "name"] unique_together = (("project", "name"), ("project", "slug")) - permissions = ( - ("view_taskstatus", "Can view task status"), - ) def __str__(self): return self.name @@ -621,9 +648,6 @@ class Priority(models.Model): verbose_name_plural = "priorities" ordering = ["project", "order", "name"] unique_together = ("project", "name") - permissions = ( - ("view_priority", "Can view priority"), - ) def __str__(self): return self.name @@ -644,9 +668,6 @@ class Severity(models.Model): verbose_name_plural = "severities" ordering = ["project", "order", "name"] unique_together = ("project", "name") - permissions = ( - ("view_severity", "Can view severity"), - ) def __str__(self): return self.name @@ -671,9 +692,6 @@ class IssueStatus(models.Model): verbose_name_plural = "issue statuses" ordering = ["project", "order", "name"] unique_together = (("project", "name"), ("project", "slug")) - permissions = ( - ("view_issuestatus", "Can view issue status"), - ) def __str__(self): return self.name @@ -702,9 +720,6 @@ class IssueType(models.Model): verbose_name_plural = "issue types" ordering = ["project", "order", "name"] unique_together = ("project", "name") - permissions = ( - ("view_issuetype", "Can view issue type"), - ) def __str__(self): return self.name @@ -728,6 +743,8 @@ class ProjectTemplate(models.Model): blank=False, verbose_name=_("default owner's role")) + is_epics_activated = models.BooleanField(default=True, null=False, blank=True, + verbose_name=_("active epics panel")) is_backlog_activated = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("active backlog panel")) is_kanban_activated = models.BooleanField(default=False, null=False, blank=True, @@ -743,6 +760,7 @@ class ProjectTemplate(models.Model): verbose_name=_("videoconference extra data")) default_options = JsonField(null=True, blank=True, verbose_name=_("default options")) + epic_statuses = JsonField(null=True, blank=True, verbose_name=_("epic statuses")) us_statuses = JsonField(null=True, blank=True, verbose_name=_("us statuses")) points = JsonField(null=True, blank=True, verbose_name=_("points")) task_statuses = JsonField(null=True, blank=True, verbose_name=_("task statuses")) @@ -770,6 +788,7 @@ class ProjectTemplate(models.Model): super().save(*args, **kwargs) def load_data_from_project(self, project): + self.is_epics_activated = project.is_epics_activated self.is_backlog_activated = project.is_backlog_activated self.is_kanban_activated = project.is_kanban_activated self.is_wiki_activated = project.is_wiki_activated @@ -779,6 +798,7 @@ class ProjectTemplate(models.Model): self.default_options = { "points": getattr(project.default_points, "name", None), + "epic_status": getattr(project.default_epic_status, "name", None), "us_status": getattr(project.default_us_status, "name", None), "task_status": getattr(project.default_task_status, "name", None), "issue_status": getattr(project.default_issue_status, "name", None), @@ -787,6 +807,16 @@ class ProjectTemplate(models.Model): "severity": getattr(project.default_severity, "name", None) } + self.epic_statuses = [] + for epic_status in project.epic_statuses.all(): + self.epic_statuses.append({ + "name": epic_status.name, + "slug": epic_status.slug, + "is_closed": epic_status.is_closed, + "color": epic_status.color, + "order": epic_status.order, + }) + self.us_statuses = [] for us_status in project.us_statuses.all(): self.us_statuses.append({ @@ -874,6 +904,7 @@ class ProjectTemplate(models.Model): raise Exception("Project need an id (must be a saved project)") project.creation_template = self + project.is_epics_activated = self.is_epics_activated project.is_backlog_activated = self.is_backlog_activated project.is_kanban_activated = self.is_kanban_activated project.is_wiki_activated = self.is_wiki_activated @@ -881,6 +912,16 @@ class ProjectTemplate(models.Model): project.videoconferences = self.videoconferences project.videoconferences_extra_data = self.videoconferences_extra_data + for epic_status in self.epic_statuses: + EpicStatus.objects.create( + name=epic_status["name"], + slug=epic_status["slug"], + is_closed=epic_status["is_closed"], + color=epic_status["color"], + order=epic_status["order"], + project=project + ) + for us_status in self.us_statuses: UserStoryStatus.objects.create( name=us_status["name"], @@ -955,12 +996,16 @@ class ProjectTemplate(models.Model): permissions=role['permissions'] ) - if self.points: - project.default_points = Points.objects.get(name=self.default_options["points"], - project=project) + if self.epic_statuses: + project.default_epic_status = EpicStatus.objects.get(name=self.default_options["epic_status"], + project=project) + if self.us_statuses: project.default_us_status = UserStoryStatus.objects.get(name=self.default_options["us_status"], project=project) + if self.points: + project.default_points = Points.objects.get(name=self.default_options["points"], + project=project) if self.task_statuses: project.default_task_status = TaskStatus.objects.get(name=self.default_options["task_status"], diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 0cc95427..5e3b44db 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -109,6 +109,18 @@ class MembershipPermission(TaigaResourcePermission): resend_invitation_perms = IsProjectAdmin() +# Epics + +class EpicStatusPermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectAdmin() + + # User Stories class PointsPermission(TaigaResourcePermission): diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index f50c0daf..43369621 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -37,11 +37,13 @@ from .notifications.choices import NotifyLevel # Custom values for selectors ###################################################### -class PointsSerializer(serializers.LightSerializer): +class EpicStatusSerializer(serializers.LightSerializer): id = Field() name = I18NField() + slug = Field() order = Field() - value = Field() + is_closed = Field() + color = Field() project = Field(attr="project_id") @@ -57,6 +59,14 @@ class UserStoryStatusSerializer(serializers.LightSerializer): project = Field(attr="project_id") +class PointsSerializer(serializers.LightSerializer): + id = Field() + name = I18NField() + order = Field() + value = Field() + project = Field(attr="project_id") + + class TaskStatusSerializer(serializers.LightSerializer): id = Field() name = I18NField() diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index e8a93623..3be0a9d8 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -19,7 +19,7 @@ # This makes all code that import services works and # is not the baddest practice ;) -from .bulk_update_order import update_projects_order_in_bulk +from .bulk_update_order import apply_order_updates from .bulk_update_order import bulk_update_severity_order from .bulk_update_order import bulk_update_priority_order from .bulk_update_order import bulk_update_issue_type_order @@ -27,7 +27,8 @@ from .bulk_update_order import bulk_update_issue_status_order from .bulk_update_order import bulk_update_task_status_order from .bulk_update_order import bulk_update_points_order from .bulk_update_order import bulk_update_userstory_status_order -from .bulk_update_order import apply_order_updates +from .bulk_update_order import bulk_update_epic_status_order +from .bulk_update_order import update_projects_order_in_bulk from .filters import get_all_tags diff --git a/taiga/projects/services/bulk_update_order.py b/taiga/projects/services/bulk_update_order.py index 4abf0e24..614fd507 100644 --- a/taiga/projects/services/bulk_update_order.py +++ b/taiga/projects/services/bulk_update_order.py @@ -86,6 +86,23 @@ def update_projects_order_in_bulk(bulk_data: list, field: str, user): db.update_attr_in_bulk_for_ids(memberships_orders, field, model=models.Membership) +@transaction.atomic +def bulk_update_epic_status_order(project, user, data): + cursor = connection.cursor() + + sql = """ + prepare bulk_update_order as update projects_epicstatus set "order" = $1 + where projects_epicstatus.id = $2 and + projects_epicstatus.project_id = $3; + """ + cursor.execute(sql) + for id, order in data: + cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", + (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") + cursor.close() + + @transaction.atomic def bulk_update_userstory_status_order(project, user, data): cursor = connection.cursor() diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py index 271a511c..55dc77fa 100644 --- a/taiga/projects/utils.py +++ b/taiga/projects/utils.py @@ -117,6 +117,26 @@ def attach_notify_policies(queryset, as_field="notify_policies_attr"): return queryset +def attach_epic_statuses(queryset, as_field="epic_statuses_attr"): + """Attach a json epic statuses representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the epic statuses as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(projects_epicstatus)) + FROM projects_epicstatus + WHERE + projects_epicstatus.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + def attach_userstory_statuses(queryset, as_field="userstory_statuses_attr"): """Attach a json userstory statuses representation to each object of the queryset. @@ -443,6 +463,7 @@ def attach_extra_info(queryset, user=None): queryset = attach_members(queryset) queryset = attach_closed_milestones(queryset) queryset = attach_notify_policies(queryset) + queryset = attach_epic_statuses(queryset) queryset = attach_userstory_statuses(queryset) queryset = attach_points(queryset) queryset = attach_task_statuses(queryset) diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index de06c05c..fdf917ea 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -86,9 +86,9 @@ class TaskStatusExistsValidator: # Custom values for selectors ###################################################### -class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): +class EpicStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): class Meta: - model = models.Points + model = models.EpicStatus class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): @@ -96,6 +96,11 @@ class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.Mode model = models.UserStoryStatus +class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.Points + + class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator): class Meta: model = models.TaskStatus diff --git a/taiga/users/migrations/0022_auto_20160629_1443.py b/taiga/users/migrations/0022_auto_20160629_1443.py new file mode 100644 index 00000000..2acaf944 --- /dev/null +++ b/taiga/users/migrations/0022_auto_20160629_1443.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-29 14:43 +from __future__ import unicode_literals + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0021_auto_20160614_1201'), + ] + + operations = [ + migrations.AlterField( + model_name='role', + name='permissions', + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epic', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'), + ), + ] diff --git a/tests/factories.py b/tests/factories.py index 379556ca..1f9d2848 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -229,22 +229,17 @@ class StorageEntryFactory(Factory): value = factory.Sequence(lambda n: {"value": "value-{}".format(n)}) -class UserStoryStatusFactory(Factory): +class EpicFactory(Factory): class Meta: - model = "projects.UserStoryStatus" + model = "epics.Epic" strategy = factory.CREATE_STRATEGY - name = factory.Sequence(lambda n: "User Story status {}".format(n)) - project = factory.SubFactory("tests.factories.ProjectFactory") - - -class TaskStatusFactory(Factory): - class Meta: - model = "projects.TaskStatus" - strategy = factory.CREATE_STRATEGY - - name = factory.Sequence(lambda n: "Task status {}".format(n)) + ref = factory.Sequence(lambda n: n) project = factory.SubFactory("tests.factories.ProjectFactory") + owner = factory.SubFactory("tests.factories.UserFactory") + subject = factory.Sequence(lambda n: "User Story {}".format(n)) + description = factory.Sequence(lambda n: "User Story {} description".format(n)) + status = factory.SubFactory("tests.factories.EpicStatusFactory") class MilestoneFactory(Factory): @@ -330,6 +325,33 @@ class WikiLinkFactory(Factory): order = factory.Sequence(lambda n: n) +class EpicStatusFactory(Factory): + class Meta: + model = "projects.EpicStatus" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "Epic status {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class UserStoryStatusFactory(Factory): + class Meta: + model = "projects.UserStoryStatus" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "User Story status {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + +class TaskStatusFactory(Factory): + class Meta: + model = "projects.TaskStatus" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "Task status {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + class IssueStatusFactory(Factory): class Meta: model = "projects.IssueStatus" diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index 77a0b754..75c0f39d 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -115,6 +115,11 @@ def data(): user=m.project_owner, is_admin=True) + m.public_epic_status = f.EpicStatusFactory(project=m.public_project) + m.private_epic_status1 = f.EpicStatusFactory(project=m.private_project1) + m.private_epic_status2 = f.EpicStatusFactory(project=m.private_project2) + m.blocked_epic_status = f.EpicStatusFactory(project=m.blocked_project) + m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) m.private_points2 = f.PointsFactory(project=m.private_project2) @@ -155,6 +160,10 @@ def data(): return m +##################################################### +# Roles +##################################################### + def test_roles_retrieve(client, data): public_url = reverse('roles-detail', kwargs={"pk": data.public_project.roles.all()[0].pk}) private1_url = reverse('roles-detail', kwargs={"pk": data.private_project1.roles.all()[0].pk}) @@ -299,6 +308,198 @@ def test_roles_patch(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Epic Status +##################################################### + +def test_epic_status_retrieve(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_status_update(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_status_data = serializers.EpicStatusSerializer(data.public_epic_status).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', public_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_status_data = serializers.EpicStatusSerializer(data.private_epic_status1).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', private1_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_status_data = serializers.EpicStatusSerializer(data.private_epic_status2).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', private2_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_status_data = serializers.EpicStatusSerializer(data.blocked_epic_status).data + epic_status_data["name"] = "test" + epic_status_data = json.dumps(epic_status_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_status_data, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_status_delete(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_status_list(client, data): + url = reverse('epic-statuses-list') + + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + 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 response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 2 + 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) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.get(url) + projects_data = json.loads(response.content.decode('utf-8')) + assert len(projects_data) == 4 + assert response.status_code == 200 + + +def test_epic_status_patch(client, data): + public_url = reverse('epic-statuses-detail', kwargs={"pk": data.public_epic_status.pk}) + private1_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status1.pk}) + private2_url = reverse('epic-statuses-detail', kwargs={"pk": data.private_epic_status2.pk}) + blocked_url = reverse('epic-statuses-detail', kwargs={"pk": data.blocked_epic_status.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_status_action_bulk_update_order(client, data): + url = reverse('epic-statuses-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_statuses": [(1, 2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + + +##################################################### +# Points +##################################################### + def test_points_retrieve(client, data): public_url = reverse('points-detail', kwargs={"pk": data.public_points.pk}) private1_url = reverse('points-detail', kwargs={"pk": data.private_points1.pk}) @@ -483,6 +684,10 @@ def test_points_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# User Story Status +##################################################### + def test_user_story_status_retrieve(client, data): public_url = reverse('userstory-statuses-detail', kwargs={"pk": data.public_user_story_status.pk}) private1_url = reverse('userstory-statuses-detail', kwargs={"pk": data.private_user_story_status1.pk}) @@ -570,7 +775,6 @@ def test_user_story_status_delete(client, data): assert results == [401, 403, 403, 403, 451] - def test_user_story_status_list(client, data): url = reverse('userstory-statuses-list') @@ -668,6 +872,10 @@ def test_user_story_status_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Task Status +##################################################### + def test_task_status_retrieve(client, data): public_url = reverse('task-statuses-detail', kwargs={"pk": data.public_task_status.pk}) private1_url = reverse('task-statuses-detail', kwargs={"pk": data.private_task_status1.pk}) @@ -755,7 +963,6 @@ def test_task_status_delete(client, data): assert results == [401, 403, 403, 403, 451] - def test_task_status_list(client, data): url = reverse('task-statuses-list') @@ -853,6 +1060,10 @@ def test_task_status_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Issue Status +##################################################### + def test_issue_status_retrieve(client, data): public_url = reverse('issue-statuses-detail', kwargs={"pk": data.public_issue_status.pk}) private1_url = reverse('issue-statuses-detail', kwargs={"pk": data.private_issue_status1.pk}) @@ -1037,6 +1248,10 @@ def test_issue_status_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Issue Type +##################################################### + def test_issue_type_retrieve(client, data): public_url = reverse('issue-types-detail', kwargs={"pk": data.public_issue_type.pk}) private1_url = reverse('issue-types-detail', kwargs={"pk": data.private_issue_type1.pk}) @@ -1221,6 +1436,10 @@ def test_issue_type_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Priority +##################################################### + def test_priority_retrieve(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) @@ -1283,6 +1502,7 @@ def test_priority_update(client, data): results = helper_test_http_method(client, 'put', blocked_url, priority_data, users) assert results == [401, 403, 403, 403, 451] + def test_priority_delete(client, data): public_url = reverse('priorities-detail', kwargs={"pk": data.public_priority.pk}) private1_url = reverse('priorities-detail', kwargs={"pk": data.private_priority1.pk}) @@ -1404,6 +1624,10 @@ def test_priority_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Severity +##################################################### + def test_severity_retrieve(client, data): public_url = reverse('severities-detail', kwargs={"pk": data.public_severity.pk}) private1_url = reverse('severities-detail', kwargs={"pk": data.private_severity1.pk}) @@ -1588,6 +1812,10 @@ def test_severity_action_bulk_update_order(client, data): assert results == [401, 403, 403, 403, 451] +##################################################### +# Memberships +##################################################### + def test_membership_retrieve(client, data): public_url = reverse('memberships-detail', kwargs={"pk": data.public_membership.pk}) private1_url = reverse('memberships-detail', kwargs={"pk": data.private_membership1.pk}) @@ -1859,6 +2087,10 @@ def test_membership_action_resend_invitation(client, data): assert results == [404, 404, 404, 403, 451] +##################################################### +# Project Templates +##################################################### + def test_project_template_retrieve(client, data): url = reverse('project-templates-detail', kwargs={"pk": data.project_template.pk}) @@ -1935,6 +2167,10 @@ def test_project_template_patch(client, data): assert results == [401, 403, 200] +##################################################### +# Tags +##################################################### + def test_create_tag(client, data): users = [ None, From 389a18026bc766a61dc4f61353db625de649f887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 30 Jun 2016 13:27:48 +0200 Subject: [PATCH 159/261] Epic custom attributes values --- taiga/projects/custom_attributes/admin.py | 5 + taiga/projects/custom_attributes/api.py | 26 +++ .../migrations/0008_auto_20160630_0849.py | 84 ++++++++ taiga/projects/custom_attributes/models.py | 28 ++- .../projects/custom_attributes/permissions.py | 20 ++ .../projects/custom_attributes/serializers.py | 8 + taiga/projects/custom_attributes/services.py | 17 ++ taiga/projects/custom_attributes/signals.py | 6 + .../projects/custom_attributes/validators.py | 14 ++ taiga/projects/epics/apps.py | 14 ++ .../management/commands/sample_data.py | 22 +- taiga/projects/serializers.py | 31 ++- taiga/projects/utils.py | 21 ++ taiga/routers.py | 71 +++++-- tests/factories.py | 1 + .../test_custom_attributes_epics.py | 201 ++++++++++++++++++ 16 files changed, 532 insertions(+), 37 deletions(-) create mode 100644 taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py create mode 100644 tests/integration/test_custom_attributes_epics.py diff --git a/taiga/projects/custom_attributes/admin.py b/taiga/projects/custom_attributes/admin.py index fca94b96..ffa676d5 100644 --- a/taiga/projects/custom_attributes/admin.py +++ b/taiga/projects/custom_attributes/admin.py @@ -38,6 +38,11 @@ class BaseCustomAttributeAdmin: raw_id_fields = ["project"] +@admin.register(models.EpicCustomAttribute) +class EpicCustomAttributeAdmin(BaseCustomAttributeAdmin, admin.ModelAdmin): + pass + + @admin.register(models.UserStoryCustomAttribute) class UserStoryCustomAttributeAdmin(BaseCustomAttributeAdmin, admin.ModelAdmin): pass diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py index 2d05d186..f8e74b00 100644 --- a/taiga/projects/custom_attributes/api.py +++ b/taiga/projects/custom_attributes/api.py @@ -41,6 +41,18 @@ from . import services # Custom Attribute ViewSets ####################################################### +class EpicCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): + model = models.EpicCustomAttribute + serializer_class = serializers.EpicCustomAttributeSerializer + validator_class = validators.EpicCustomAttributeValidator + permission_classes = (permissions.EpicCustomAttributePermission,) + filter_backends = (filters.CanViewProjectFilterBackend,) + filter_fields = ("project",) + bulk_update_param = "bulk_epic_custom_attributes" + bulk_update_perm = "change_epic_custom_attributes" + bulk_update_order_action = services.bulk_update_epic_custom_attribute_order + + class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): model = models.UserStoryCustomAttribute serializer_class = serializers.UserStoryCustomAttributeSerializer @@ -87,6 +99,20 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, return getattr(obj, self.content_object) +class EpicCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): + model = models.EpicCustomAttributesValues + serializer_class = serializers.EpicCustomAttributesValuesSerializer + validator_class = validators.EpicCustomAttributesValuesValidator + permission_classes = (permissions.EpicCustomAttributesValuesPermission,) + lookup_field = "epic_id" + content_object = "epic" + + def get_queryset(self): + qs = self.model.objects.all() + qs = qs.select_related("epic", "epic__project") + return qs + + class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): model = models.UserStoryCustomAttributesValues serializer_class = serializers.UserStoryCustomAttributesValuesSerializer diff --git a/taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py b/taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py new file mode 100644 index 00000000..bcb7668c --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-06-30 08:49 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import django_pgjson.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('epics', '0001_initial'), + ('projects', '0049_auto_20160629_1443'), + ('custom_attributes', '0007_auto_20160208_1751'), + ] + + operations = [ + migrations.AlterModelOptions( + name='issuecustomattributesvalues', + options={'ordering': ['id'], 'verbose_name': 'issue custom attributes values', 'verbose_name_plural': 'issue custom attributes values'}, + ), + migrations.AlterModelOptions( + name='taskcustomattributesvalues', + options={'ordering': ['id'], 'verbose_name': 'task custom attributes values', 'verbose_name_plural': 'task custom attributes values'}, + ), + migrations.AlterModelOptions( + name='userstorycustomattributesvalues', + options={'ordering': ['id'], 'verbose_name': 'user story custom attributes values', 'verbose_name_plural': 'user story custom attributes values'}, + ), + + migrations.CreateModel( + name='EpicCustomAttribute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='name')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('type', models.CharField(choices=[('text', 'Text'), ('multiline', 'Multi-Line Text'), ('date', 'Date'), ('url', 'Url')], default='text', max_length=16, verbose_name='type')), + ('order', models.IntegerField(default=10000, verbose_name='order')), + ('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')), + ('modified_date', models.DateTimeField(verbose_name='modified date')), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epiccustomattributes', to='projects.Project', verbose_name='project')), + ], + options={ + 'verbose_name_plural': 'epic custom attributes', + 'verbose_name': 'epic custom attribute', + 'abstract': False, + 'ordering': ['project', 'order', 'name'], + }, + ), + migrations.CreateModel( + name='EpicCustomAttributesValues', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.IntegerField(default=1, verbose_name='version')), + ('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='values')), + ('epic', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_attributes_values', to='epics.Epic', verbose_name='epic')), + ], + options={ + 'verbose_name_plural': 'epic custom attributes values', + 'verbose_name': 'epic custom attributes values', + 'abstract': False, + 'ordering': ['id'], + }, + ), + migrations.AlterUniqueTogether( + name='epiccustomattribute', + unique_together=set([('project', 'name')]), + ), + + # Trigger: Clean epiccustomattributes values before remove a epiccustomattribute + migrations.RunSQL( + """ + CREATE TRIGGER "update_epiccustomvalues_after_remove_epiccustomattribute" + AFTER DELETE ON custom_attributes_epiccustomattribute + FOR EACH ROW + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_epiccustomattributesvalues'); + """, + reverse_sql="""DROP TRIGGER IF EXISTS "update_epiccustomvalues_after_remove_epiccustomattribute" + ON custom_attributes_epiccustomattribute + CASCADE;""" + ), + ] diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 5fe3c6a0..8b5747f0 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -31,7 +31,6 @@ from . import choices # Custom Attribute Models ####################################################### - class AbstractCustomAttribute(models.Model): name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) @@ -63,6 +62,12 @@ class AbstractCustomAttribute(models.Model): return super().save(*args, **kwargs) +class EpicCustomAttribute(AbstractCustomAttribute): + class Meta(AbstractCustomAttribute.Meta): + verbose_name = "epic custom attribute" + verbose_name_plural = "epic custom attributes" + + class UserStoryCustomAttribute(AbstractCustomAttribute): class Meta(AbstractCustomAttribute.Meta): verbose_name = "user story custom attribute" @@ -93,13 +98,28 @@ class AbstractCustomAttributesValues(OCCModelMixin, models.Model): ordering = ["id"] +class EpicCustomAttributesValues(AbstractCustomAttributesValues): + epic = models.OneToOneField("epics.Epic", + null=False, blank=False, related_name="custom_attributes_values", + verbose_name=_("epic")) + + class Meta(AbstractCustomAttributesValues.Meta): + verbose_name = "epic custom attributes values" + verbose_name_plural = "epic custom attributes values" + + @property + def project(self): + # NOTE: This property simplifies checking permissions + return self.epic.project + + class UserStoryCustomAttributesValues(AbstractCustomAttributesValues): user_story = models.OneToOneField("userstories.UserStory", null=False, blank=False, related_name="custom_attributes_values", verbose_name=_("user story")) class Meta(AbstractCustomAttributesValues.Meta): - verbose_name = "user story ustom attributes values" + verbose_name = "user story custom attributes values" verbose_name_plural = "user story custom attributes values" index_together = [("user_story",)] @@ -115,7 +135,7 @@ class TaskCustomAttributesValues(AbstractCustomAttributesValues): verbose_name=_("task")) class Meta(AbstractCustomAttributesValues.Meta): - verbose_name = "task ustom attributes values" + verbose_name = "task custom attributes values" verbose_name_plural = "task custom attributes values" index_together = [("task",)] @@ -131,7 +151,7 @@ class IssueCustomAttributesValues(AbstractCustomAttributesValues): verbose_name=_("issue")) class Meta(AbstractCustomAttributesValues.Meta): - verbose_name = "issue ustom attributes values" + verbose_name = "issue custom attributes values" verbose_name_plural = "issue custom attributes values" index_together = [("issue",)] diff --git a/taiga/projects/custom_attributes/permissions.py b/taiga/projects/custom_attributes/permissions.py index 5771cce4..ffc6a04c 100644 --- a/taiga/projects/custom_attributes/permissions.py +++ b/taiga/projects/custom_attributes/permissions.py @@ -27,6 +27,18 @@ from taiga.base.api.permissions import IsSuperUser # Custom Attribute Permissions ####################################################### +class EpicCustomAttributePermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_project') + create_perms = IsProjectAdmin() + update_perms = IsProjectAdmin() + partial_update_perms = IsProjectAdmin() + destroy_perms = IsProjectAdmin() + list_perms = AllowAny() + bulk_update_order_perms = IsProjectAdmin() + + class UserStoryCustomAttributePermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None @@ -67,6 +79,14 @@ class IssueCustomAttributePermission(TaigaResourcePermission): # Custom Attributes Values Permissions ####################################################### +class EpicCustomAttributesValuesPermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_us') + update_perms = HasProjectPerm('modify_us') + partial_update_perms = HasProjectPerm('modify_us') + + class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py index afc5ff72..10e9c756 100644 --- a/taiga/projects/custom_attributes/serializers.py +++ b/taiga/projects/custom_attributes/serializers.py @@ -36,6 +36,10 @@ class BaseCustomAttributeSerializer(serializers.LightSerializer): modified_date = Field() +class EpicCustomAttributeSerializer(BaseCustomAttributeSerializer): + pass + + class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer): pass @@ -56,6 +60,10 @@ class BaseCustomAttributesValuesSerializer(serializers.LightSerializer): version = Field() +class EpicCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): + epic = Field(attr="epic.id") + + class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): user_story = Field(attr="user_story.id") diff --git a/taiga/projects/custom_attributes/services.py b/taiga/projects/custom_attributes/services.py index c957c5dc..4a30305e 100644 --- a/taiga/projects/custom_attributes/services.py +++ b/taiga/projects/custom_attributes/services.py @@ -20,6 +20,23 @@ from django.db import transaction from django.db import connection +@transaction.atomic +def bulk_update_epic_custom_attribute_order(project, user, data): + cursor = connection.cursor() + + sql = """ + prepare bulk_update_order as update custom_attributes_epiccustomattribute set "order" = $1 + where custom_attributes_epiccustomattribute.id = $2 and + custom_attributes_epiccustomattribute.project_id = $3; + """ + cursor.execute(sql) + for id, order in data: + cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);", + (order, id, project.id)) + cursor.execute("DEALLOCATE bulk_update_order") + cursor.close() + + @transaction.atomic def bulk_update_userstory_custom_attribute_order(project, user, data): cursor = connection.cursor() diff --git a/taiga/projects/custom_attributes/signals.py b/taiga/projects/custom_attributes/signals.py index 72b715a7..96e74e9e 100644 --- a/taiga/projects/custom_attributes/signals.py +++ b/taiga/projects/custom_attributes/signals.py @@ -19,6 +19,12 @@ from . import models +def create_custom_attribute_value_when_create_epic(sender, instance, created, **kwargs): + if created: + models.EpicCustomAttributesValues.objects.get_or_create(epic=instance, + defaults={"attributes_values":{}}) + + def create_custom_attribute_value_when_create_user_story(sender, instance, created, **kwargs): if created: models.UserStoryCustomAttributesValues.objects.get_or_create(user_story=instance, diff --git a/taiga/projects/custom_attributes/validators.py b/taiga/projects/custom_attributes/validators.py index 6663de5d..4169eee6 100644 --- a/taiga/projects/custom_attributes/validators.py +++ b/taiga/projects/custom_attributes/validators.py @@ -66,6 +66,11 @@ class BaseCustomAttributeValidator(ModelValidator): return self._validate_integrity_between_project_and_name(attrs, source) +class EpicCustomAttributeValidator(BaseCustomAttributeValidator): + class Meta(BaseCustomAttributeValidator.Meta): + model = models.EpicCustomAttribute + + class UserStoryCustomAttributeValidator(BaseCustomAttributeValidator): class Meta(BaseCustomAttributeValidator.Meta): model = models.UserStoryCustomAttribute @@ -121,6 +126,15 @@ class BaseCustomAttributesValuesValidator(ModelValidator): return attrs +class EpicCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator): + _custom_attribute_model = models.EpicCustomAttribute + _container_model = "epics.Epic" + _container_field = "epic" + + class Meta(BaseCustomAttributesValuesValidator.Meta): + model = models.EpicCustomAttributesValues + + class UserStoryCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator): _custom_attribute_model = models.UserStoryCustomAttribute _container_model = "userstories.UserStory" diff --git a/taiga/projects/epics/apps.py b/taiga/projects/epics/apps.py index cff4d699..5389b98a 100644 --- a/taiga/projects/epics/apps.py +++ b/taiga/projects/epics/apps.py @@ -30,8 +30,16 @@ def connect_epics_signals(): dispatch_uid="tags_normalization_epic") +def connect_epics_custom_attributes_signals(): + from taiga.projects.custom_attributes import signals as custom_attributes_handlers + signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_epic, + sender=apps.get_model("epics", "Epic"), + dispatch_uid="create_custom_attribute_value_when_create_epic") + + def connect_all_epics_signals(): connect_epics_signals() + connect_epics_custom_attributes_signals() def disconnect_epics_signals(): @@ -39,8 +47,14 @@ def disconnect_epics_signals(): dispatch_uid="tags_normalization") +def disconnect_epics_custom_attributes_signals(): + signals.post_save.disconnect(sender=apps.get_model("epics", "Epic"), + dispatch_uid="create_custom_attribute_value_when_create_epic") + + def disconnect_all_epics_signals(): disconnect_epics_signals() + disconnect_epics_custom_attributes_signals() class EpicsAppConfig(AppConfig): diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index b1726496..70ec16a1 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -131,7 +131,7 @@ LOOKING_FOR_PEOPLE_PROJECTS_POSITIONS = [0, 1, 2] class Command(BaseCommand): sd = SampleDataHelper(seed=12345678901) - @transaction.atomic + #@transaction.atomic def handle(self, *args, **options): # Prevent events emission when sample data is running disconnect_events_signals() @@ -193,6 +193,13 @@ class Command(BaseCommand): # added custom attributes names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + EpicCustomAttribute.objects.create(name=name, + description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], + project=project, + order=i) + names = set([self.sd.words(1, 3) for i in range(1, 6)]) for name in names: UserStoryCustomAttribute.objects.create(name=name, description=self.sd.words(3, 12), @@ -511,14 +518,13 @@ class Command(BaseCommand): status=self.sd.db_object_from_queryset(project.epic_statuses.filter( is_closed=False)), tags=self.sd.words(1, 3).split(" ")) + epic.save() - # TODO: Epic custom attributes - #custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca - # in project.epiccustomattributes.all() if self.sd.boolean()} - #if custom_attributes_values: - # epic.custom_attributes_values.attributes_values = custom_attributes_values - # epic.custom_attributes_values.save() - + custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca + in project.epiccustomattributes.all() if self.sd.boolean()} + if custom_attributes_values: + epic.custom_attributes_values.attributes_values = custom_attributes_values + epic.custom_attributes_values.save() for i in range(self.sd.int(*NUM_ATTACHMENTS)): attachment = self.create_attachment(epic, i+1) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 43369621..9a9c639d 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -30,6 +30,15 @@ from taiga.permissions.services import calculate_permissions from taiga.permissions.services import is_project_admin, is_project_owner from . import services +<<<<<<< HEAD +======= +from .custom_attributes.serializers import EpicCustomAttributeSerializer +from .custom_attributes.serializers import UserStoryCustomAttributeSerializer +from .custom_attributes.serializers import TaskCustomAttributeSerializer +from .custom_attributes.serializers import IssueCustomAttributeSerializer +from .likes.mixins.serializers import FanResourceSerializerMixin +from .mixins.serializers import ValidateDuplicatedNameInProjectMixin +>>>>>>> 5f3559d... Epic custom attributes values from .notifications.choices import NotifyLevel @@ -352,6 +361,7 @@ class ProjectSerializer(serializers.LightSerializer): class ProjectDetailSerializer(ProjectSerializer): + epic_statuses = Field(attr="epic_statuses_attr") us_statuses = Field(attr="userstory_statuses_attr") points = Field(attr="points_attr") task_statuses = Field(attr="task_statuses_attr") @@ -359,6 +369,7 @@ class ProjectDetailSerializer(ProjectSerializer): issue_types = Field(attr="issue_types_attr") priorities = Field(attr="priorities_attr") severities = Field(attr="severities_attr") + epic_custom_attributes = Field(attr="epic_custom_attributes_attr") userstory_custom_attributes = Field(attr="userstory_custom_attributes_attr") task_custom_attributes = Field(attr="task_custom_attributes_attr") issue_custom_attributes = Field(attr="issue_custom_attributes_attr") @@ -385,9 +396,9 @@ class ProjectDetailSerializer(ProjectSerializer): def to_value(self, instance): # Name attributes must be translated - for attr in ["userstory_statuses_attr", "points_attr", "task_statuses_attr", - "issue_statuses_attr", "issue_types_attr", "priorities_attr", - "severities_attr", "userstory_custom_attributes_attr", + for attr in ["epic_statuses_attr", "userstory_statuses_attr", "points_attr", "task_statuses_attr", + "issue_statuses_attr", "issue_types_attr", "priorities_attr", "severities_attr", + "epic_custom_attributes_attr", "userstory_custom_attributes_attr", "task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]: assert hasattr(instance, attr), "instance must have a {} attribute".format(attr) @@ -430,8 +441,10 @@ class ProjectDetailSerializer(ProjectSerializer): return len(obj.members_attr) def get_is_out_of_owner_limits(self, obj): - assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" - assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" + assert (hasattr(obj, "private_projects_same_owner_attr"), + "instance must have a private_projects_same_owner_attr attribute" + assert (hasattr(obj, "public_projects_same_owner_attr"), + "instance must have a public_projects_same_owner_attr attribute" return services.check_if_project_is_out_of_owner_limits( obj, current_memberships=self.get_total_memberships(obj), @@ -440,8 +453,10 @@ class ProjectDetailSerializer(ProjectSerializer): ) def get_is_private_extra_info(self, obj): - assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" - assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" + assert (hasattr(obj, "private_projects_same_owner_attr"), + "instance must have a private_projects_same_owner_attr attribute" + assert (hasattr(obj, "public_projects_same_owner_attr"), + "instance must have a public_projects_same_owner_attr attribute" return services.check_if_project_privacity_can_be_changed( obj, current_memberships=self.get_total_memberships(obj), @@ -466,6 +481,7 @@ class ProjectTemplateSerializer(serializers.LightSerializer): created_date = Field() modified_date = Field() default_owner_role = Field() + is_epics_activated = Field() is_backlog_activated = Field() is_kanban_activated = Field() is_wiki_activated = Field() @@ -473,6 +489,7 @@ class ProjectTemplateSerializer(serializers.LightSerializer): videoconferences = Field() videoconferences_extra_data = Field() default_options = Field() + epic_statuses = Field() us_statuses = Field() points = Field() task_statuses = Field() diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py index 55dc77fa..d56a96c9 100644 --- a/taiga/projects/utils.py +++ b/taiga/projects/utils.py @@ -277,6 +277,26 @@ def attach_severities(queryset, as_field="severities_attr"): return queryset +def attach_epic_custom_attributes(queryset, as_field="epic_custom_attributes_attr"): + """Attach a json epic custom attributes representation to each object of the queryset. + + :param queryset: A Django projects queryset object. + :param as_field: Attach the epic custom attributes as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(custom_attributes_epiccustomattribute)) + FROM custom_attributes_epiccustomattribute + WHERE + custom_attributes_epiccustomattribute.project_id = {tbl}.id + """ + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + def attach_userstory_custom_attributes(queryset, as_field="userstory_custom_attributes_attr"): """Attach a json userstory custom attributes representation to each object of the queryset. @@ -471,6 +491,7 @@ def attach_extra_info(queryset, user=None): queryset = attach_issue_types(queryset) queryset = attach_priorities(queryset) queryset = attach_severities(queryset) + queryset = attach_epic_custom_attributes(queryset) queryset = attach_userstory_custom_attributes(queryset) queryset = attach_task_custom_attributes(queryset) queryset = attach_issue_custom_attributes(queryset) diff --git a/taiga/routers.py b/taiga/routers.py index 24974b74..d8bc8b00 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -54,6 +54,7 @@ from taiga.projects.api import ProjectFansViewSet from taiga.projects.api import ProjectWatchersViewSet from taiga.projects.api import MembershipViewSet from taiga.projects.api import InvitationViewSet +from taiga.projects.api import EpicStatusViewSet from taiga.projects.api import UserStoryStatusViewSet from taiga.projects.api import PointsViewSet from taiga.projects.api import TaskStatusViewSet @@ -69,6 +70,7 @@ router.register(r"projects/(?P\d+)/watchers", ProjectWatchersViewSe router.register(r"project-templates", ProjectTemplateViewSet, base_name="project-templates") router.register(r"memberships", MembershipViewSet, base_name="memberships") router.register(r"invitations", InvitationViewSet, base_name="invitations") +router.register(r"epic-statuses", EpicStatusViewSet, base_name="epic-statuses") router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses") router.register(r"points", PointsViewSet, base_name="points") router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses") @@ -79,13 +81,18 @@ router.register(r"severities",SeverityViewSet , base_name="severities") # Custom Attributes +from taiga.projects.custom_attributes.api import EpicCustomAttributeViewSet from taiga.projects.custom_attributes.api import UserStoryCustomAttributeViewSet from taiga.projects.custom_attributes.api import TaskCustomAttributeViewSet from taiga.projects.custom_attributes.api import IssueCustomAttributeViewSet + +from taiga.projects.custom_attributes.api import EpicCustomAttributesValuesViewSet from taiga.projects.custom_attributes.api import UserStoryCustomAttributesValuesViewSet from taiga.projects.custom_attributes.api import TaskCustomAttributesValuesViewSet from taiga.projects.custom_attributes.api import IssueCustomAttributesValuesViewSet +router.register(r"epic-custom-attributes", EpicCustomAttributeViewSet, + base_name="epic-custom-attributes") router.register(r"userstory-custom-attributes", UserStoryCustomAttributeViewSet, base_name="userstory-custom-attributes") router.register(r"task-custom-attributes", TaskCustomAttributeViewSet, @@ -93,6 +100,8 @@ router.register(r"task-custom-attributes", TaskCustomAttributeViewSet, router.register(r"issue-custom-attributes", IssueCustomAttributeViewSet, base_name="issue-custom-attributes") +router.register(r"epics/custom-attributes-values", EpicCustomAttributesValuesViewSet, + base_name="epic-custom-attributes-values") router.register(r"userstories/custom-attributes-values", UserStoryCustomAttributesValuesViewSet, base_name="userstory-custom-attributes-values") router.register(r"tasks/custom-attributes-values", TaskCustomAttributesValuesViewSet, @@ -114,50 +123,76 @@ router.register(r"resolver", ResolverViewSet, base_name="resolver") # Attachments +from taiga.projects.attachments.api import EpicAttachmentViewSet from taiga.projects.attachments.api import UserStoryAttachmentViewSet from taiga.projects.attachments.api import IssueAttachmentViewSet from taiga.projects.attachments.api import TaskAttachmentViewSet from taiga.projects.attachments.api import WikiAttachmentViewSet +router.register(r"epics/attachments", EpicAttachmentViewSet, + base_name="epic-attachments") router.register(r"userstories/attachments", UserStoryAttachmentViewSet, base_name="userstory-attachments") -router.register(r"tasks/attachments", TaskAttachmentViewSet, base_name="task-attachments") -router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-attachments") -router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments") +router.register(r"tasks/attachments", TaskAttachmentViewSet, + base_name="task-attachments") +router.register(r"issues/attachments", IssueAttachmentViewSet, + base_name="issue-attachments") +router.register(r"wiki/attachments", WikiAttachmentViewSet, + base_name="wiki-attachments") # Project components from taiga.projects.milestones.api import MilestoneViewSet from taiga.projects.milestones.api import MilestoneWatchersViewSet + from taiga.projects.userstories.api import UserStoryViewSet from taiga.projects.userstories.api import UserStoryVotersViewSet from taiga.projects.userstories.api import UserStoryWatchersViewSet + from taiga.projects.tasks.api import TaskViewSet from taiga.projects.tasks.api import TaskVotersViewSet from taiga.projects.tasks.api import TaskWatchersViewSet + from taiga.projects.issues.api import IssueViewSet from taiga.projects.issues.api import IssueVotersViewSet from taiga.projects.issues.api import IssueWatchersViewSet + from taiga.projects.wiki.api import WikiViewSet from taiga.projects.wiki.api import WikiLinkViewSet from taiga.projects.wiki.api import WikiWatchersViewSet -router.register(r"milestones", MilestoneViewSet, base_name="milestones") -router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers") -router.register(r"userstories", UserStoryViewSet, base_name="userstories") -router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, base_name="userstory-voters") -router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet, base_name="userstory-watchers") -router.register(r"tasks", TaskViewSet, base_name="tasks") -router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, base_name="task-voters") -router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet, base_name="task-watchers") -router.register(r"issues", IssueViewSet, base_name="issues") -router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet, base_name="issue-voters") -router.register(r"issues/(?P\d+)/watchers", IssueWatchersViewSet, base_name="issue-watchers") -router.register(r"wiki", WikiViewSet, base_name="wiki") -router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet, base_name="wiki-watchers") -router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links") +router.register(r"milestones", MilestoneViewSet, + base_name="milestones") +router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, + base_name="milestone-watchers") +router.register(r"userstories", UserStoryViewSet, + base_name="userstories") +router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, + base_name="userstory-voters") +router.register(r"userstories/(?P\d+)/watchers", UserStoryWatchersViewSet, + base_name="userstory-watchers") +router.register(r"tasks", TaskViewSet, + base_name="tasks") +router.register(r"tasks/(?P\d+)/voters", TaskVotersViewSet, + base_name="task-voters") +router.register(r"tasks/(?P\d+)/watchers", TaskWatchersViewSet, + base_name="task-watchers") + +router.register(r"issues", IssueViewSet, + base_name="issues") +router.register(r"issues/(?P\d+)/voters", IssueVotersViewSet, + base_name="issue-voters") +router.register(r"issues/(?P\d+)/watchers", IssueWatchersViewSet, + base_name="issue-watchers") + +router.register(r"wiki", WikiViewSet, + base_name="wiki") +router.register(r"wiki/(?P\d+)/watchers", WikiWatchersViewSet, + base_name="wiki-watchers") +router.register(r"wiki-links", WikiLinkViewSet, + base_name="wiki-links") # History & Components @@ -223,11 +258,11 @@ router.register(r"exporter", ProjectExporterViewSet, base_name="exporter") # External apps from taiga.external_apps.api import Application, ApplicationToken + router.register(r"applications", Application, base_name="applications") router.register(r"application-tokens", ApplicationToken, base_name="application-tokens") - # Stats # - see taiga.stats.routers and taiga.stats.apps diff --git a/tests/factories.py b/tests/factories.py index 1f9d2848..b5fdd880 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -57,6 +57,7 @@ class ProjectTemplateFactory(Factory): slug = settings.DEFAULT_PROJECT_TEMPLATE description = factory.Sequence(lambda n: "Description {}".format(n)) + epic_statuses = [] us_statuses = [] points = [] task_statuses = [] diff --git a/tests/integration/test_custom_attributes_epics.py b/tests/integration/test_custom_attributes_epics.py new file mode 100644 index 00000000..e24f1d8f --- /dev/null +++ b/tests/integration/test_custom_attributes_epics.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.db.transaction import atomic +from django.core.urlresolvers import reverse +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +######################################################### +# Epic Custom Attributes +######################################################### + +def test_epic_custom_attribute_duplicate_name_error_on_create(client): + custom_attr_1 = f.EpicCustomAttributeFactory() + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_admin=True) + + + url = reverse("epic-custom-attributes-list") + data = {"name": custom_attr_1.name, + "project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_epic_custom_attribute_duplicate_name_error_on_update(client): + custom_attr_1 = f.EpicCustomAttributeFactory() + custom_attr_2 = f.EpicCustomAttributeFactory(project=custom_attr_1.project) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_admin=True) + + + url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"name": custom_attr_1.name} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +def test_epic_custom_attribute_duplicate_name_error_on_move_between_projects(client): + custom_attr_1 = f.EpicCustomAttributeFactory() + custom_attr_2 = f.EpicCustomAttributeFactory(name=custom_attr_1.name) + member = f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_1.project, + is_admin=True) + f.MembershipFactory(user=custom_attr_1.project.owner, + project=custom_attr_2.project, + is_admin=True) + + url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + data = {"project": custom_attr_1.project.pk} + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + + +######################################################### +# Epic Custom Attributes Values +######################################################### + +def test_epic_custom_attributes_values_when_create_us(client): + epic = f.EpicFactory() + assert epic.custom_attributes_values.attributes_values == {} + + +def test_epic_custom_attributes_values_update(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = epic.custom_attributes_values + + url = reverse("epic-custom-attributes-values-detail", args=[epic.id]) + data = { + "attributes_values": { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + }, + "version": custom_attrs_val.version + } + + + assert epic.custom_attributes_values.attributes_values == {} + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200 + assert response.data["attributes_values"] == data["attributes_values"] + epic = epic.__class__.objects.get(id=epic.id) + assert epic.custom_attributes_values.attributes_values == data["attributes_values"] + + +def test_epic_custom_attributes_values_update_with_error_invalid_key(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + + custom_attrs_val = epic.custom_attributes_values + + url = reverse("epic-custom-attributes-values-detail", args=[epic.id]) + data = { + "attributes_values": { + ct1_id: "test_1_updated", + "123456": "test_2_updated" + }, + "version": custom_attrs_val.version + } + + client.login(member.user) + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400 + +def test_epic_custom_attributes_values_delete_epic(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = epic.custom_attributes_values + + url = reverse("epics-detail", args=[epic.id]) + + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + assert not epic.__class__.objects.filter(id=epic.id).exists() + assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists() + + +######################################################### +# Test tristres triggers :-P +######################################################### + +def test_trigger_update_epiccustomvalues_afeter_remove_epiccustomattribute(client): + epic = f.EpicFactory() + member = f.MembershipFactory(user=epic.project.owner, + project=epic.project, + is_admin=True) + custom_attr_1 = f.EpicCustomAttributeFactory(project=epic.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=epic.project) + ct2_id = "{}".format(custom_attr_2.id) + + custom_attrs_val = epic.custom_attributes_values + custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"} + custom_attrs_val.save() + + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id in custom_attrs_val.attributes_values.keys() + + url = reverse("epic-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk}) + client.login(member.user) + response = client.json.delete(url) + assert response.status_code == 204 + + custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id) + assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists() + assert ct1_id in custom_attrs_val.attributes_values.keys() + assert ct2_id not in custom_attrs_val.attributes_values.keys() From a7c262ffdc39b3463f2d4f527d05e37129d36e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 1 Jul 2016 12:37:19 +0200 Subject: [PATCH 160/261] Working in history and timeline for epics (initial version) --- taiga/projects/history/api.py | 5 ++ taiga/projects/history/freeze_impl.py | 49 +++++++++++++++++++ taiga/projects/history/permissions.py | 8 +++ taiga/projects/history/services.py | 10 ++-- taiga/projects/serializers.py | 38 +++++--------- taiga/routers.py | 2 + taiga/timeline/api.py | 2 + taiga/timeline/apps.py | 6 +-- .../_rebuild_timeline_for_user_creation.py | 8 +-- .../_update_timeline_for_updated_tasks.py | 4 +- .../management/commands/rebuild_timeline.py | 12 +++-- ...rebuild_timeline_iterating_per_projects.py | 6 +-- taiga/timeline/service.py | 24 +++++++-- taiga/timeline/signals.py | 18 ++++++- taiga/timeline/timeline_implementations.py | 12 +++++ tests/factories.py | 19 +++++++ 16 files changed, 173 insertions(+), 50 deletions(-) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index db1d6bbc..17c2fa83 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -168,6 +168,11 @@ class HistoryViewSet(ReadOnlyListViewSet): return self.response_for_queryset(qs) +class EpicHistory(HistoryViewSet): + content_type = "epics.epic" + permission_classes = (permissions.EpicHistoryPermission,) + + class UserStoryHistory(HistoryViewSet): content_type = "userstories.userstory" permission_classes = (permissions.UserStoryHistoryPermission,) diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 9b2dcadc..7d5b3543 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -106,6 +106,17 @@ def milestone_values(diff): return values +def epic_values(diff): + values = _common_users_values(diff) + + if "status" in diff: + values["status"] = _get_us_status_values(diff["status"]) + + # TODO EPICS: What happen with usr stories? + + return values + + def userstory_values(diff): values = _common_users_values(diff) @@ -190,6 +201,18 @@ def extract_attachments(obj) -> list: "order": attach.order} +@as_tuple +def extract_epic_custom_attributes(obj) -> list: + with suppress(ObjectDoesNotExist): + custom_attributes_values = obj.custom_attributes_values.attributes_values + for attr in obj.project.epiccustomattributes.all(): + with suppress(KeyError): + value = custom_attributes_values[str(attr.id)] + yield {"id": attr.id, + "name": attr.name, + "value": value} + + @as_tuple def extract_user_story_custom_attributes(obj) -> list: with suppress(ObjectDoesNotExist): @@ -235,6 +258,7 @@ def project_freezer(project) -> dict: "total_milestones", "total_story_points", "tags", + "is_epics_activated", "is_backlog_activated", "is_kanban_activated", "is_wiki_activated", @@ -256,6 +280,31 @@ def milestone_freezer(milestone) -> dict: return snapshot +def epic_freezer(epic) -> dict: + snapshot = { + "ref": epic.ref, + "owner": epic.owner_id, + "status": epic.status.id if epic.status else None, + "is_closed": epic.is_closed, + "finish_date": str(epic.finish_date), + "epics_order": epic.epics_order, + "subject": epic.subject, + "description": epic.description, + "description_html": mdrender(epic.project, epic.description), + "assigned_to": epic.assigned_to_id, + "client_requirement": epic.client_requirement, + "team_requirement": epic.team_requirement, + "attachments": extract_attachments(epic), + "tags": epic.tags, + "is_blocked": epic.is_blocked, + "blocked_note": epic.blocked_note, + "blocked_note_html": mdrender(epic.project, epic.blocked_note), + "custom_attributes": extract_epic_custom_attributes(epic), + } + + return snapshot + + def userstory_freezer(us) -> dict: rp_cls = apps.get_model("userstories", "RolePoints") rpqsd = rp_cls.objects.filter(user_story=us) diff --git a/taiga/projects/history/permissions.py b/taiga/projects/history/permissions.py index e55ac3c4..40e4f98b 100644 --- a/taiga/projects/history/permissions.py +++ b/taiga/projects/history/permissions.py @@ -42,6 +42,14 @@ class IsCommentProjectAdmin(PermissionComponent): return is_project_admin(request.user, project) +class EpicHistoryPermission(TaigaResourcePermission): + retrieve_perms = HasProjectPerm('view_project') + edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() + undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter() + comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner() + + class UserStoryHistoryPermission(TaigaResourcePermission): retrieve_perms = HasProjectPerm('view_project') edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner() diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index b9526b08..54dd78fc 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -50,6 +50,7 @@ from .models import HistoryType # Freeze implementatitions from .freeze_impl import project_freezer from .freeze_impl import milestone_freezer +from .freeze_impl import epic_freezer from .freeze_impl import userstory_freezer from .freeze_impl import issue_freezer from .freeze_impl import task_freezer @@ -58,6 +59,7 @@ from .freeze_impl import wikipage_freezer from .freeze_impl import project_values from .freeze_impl import milestone_values +from .freeze_impl import epic_values from .freeze_impl import userstory_values from .freeze_impl import issue_values from .freeze_impl import task_values @@ -337,10 +339,7 @@ def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False # If diff and comment are empty, do # not create empty history entry - if (not fdiff.diff and not comment - and old_fobj is not None - and entry_type != HistoryType.delete): - + if (not fdiff.diff and not comment and old_fobj is not None and entry_type != HistoryType.delete): return None fvals = make_diff_values(typename, fdiff) @@ -394,8 +393,10 @@ def prefetch_owners_in_history_queryset(qs): return qs +# Freeze & value register register_freeze_implementation("projects.project", project_freezer) register_freeze_implementation("milestones.milestone", milestone_freezer,) +register_freeze_implementation("epics.epic", epic_freezer) register_freeze_implementation("userstories.userstory", userstory_freezer) register_freeze_implementation("issues.issue", issue_freezer) register_freeze_implementation("tasks.task", task_freezer) @@ -403,6 +404,7 @@ register_freeze_implementation("wiki.wikipage", wikipage_freezer) register_values_implementation("projects.project", project_values) register_values_implementation("milestones.milestone", milestone_values) +register_values_implementation("epics.epic", epic_values) register_values_implementation("userstories.userstory", userstory_values) register_values_implementation("issues.issue", issue_values) register_values_implementation("tasks.task", task_values) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 9a9c639d..58999578 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -30,15 +30,6 @@ from taiga.permissions.services import calculate_permissions from taiga.permissions.services import is_project_admin, is_project_owner from . import services -<<<<<<< HEAD -======= -from .custom_attributes.serializers import EpicCustomAttributeSerializer -from .custom_attributes.serializers import UserStoryCustomAttributeSerializer -from .custom_attributes.serializers import TaskCustomAttributeSerializer -from .custom_attributes.serializers import IssueCustomAttributeSerializer -from .likes.mixins.serializers import FanResourceSerializerMixin -from .mixins.serializers import ValidateDuplicatedNameInProjectMixin ->>>>>>> 5f3559d... Epic custom attributes values from .notifications.choices import NotifyLevel @@ -222,6 +213,7 @@ class ProjectSerializer(serializers.LightSerializer): members = MethodField() total_milestones = Field() total_story_points = Field() + is_epics_activated = Field() is_backlog_activated = Field() is_kanban_activated = Field() is_wiki_activated = Field() @@ -249,6 +241,7 @@ class ProjectSerializer(serializers.LightSerializer): tags = Field() tags_colors = MethodField() + default_epic_status = Field(attr="default_epic_status_id") default_points = Field(attr="default_points_id") default_us_status = Field(attr="default_us_status_id") default_task_status = Field(attr="default_task_status_id") @@ -300,14 +293,13 @@ class ProjectSerializer(serializers.LightSerializer): def get_my_permissions(self, obj): if "request" in self.context: user = self.context["request"].user - return calculate_permissions( - is_authenticated=user.is_authenticated(), - is_superuser=user.is_superuser, - is_member=self.get_i_am_member(obj), - is_admin=self.get_i_am_admin(obj), - role_permissions=obj.my_role_permissions_attr, - anon_permissions=obj.anon_permissions, - public_permissions=obj.public_permissions) + return calculate_permissions(is_authenticated=user.is_authenticated(), + is_superuser=user.is_superuser, + is_member=self.get_i_am_member(obj), + is_admin=self.get_i_am_admin(obj), + role_permissions=obj.my_role_permissions_attr, + anon_permissions=obj.anon_permissions, + public_permissions=obj.public_permissions) return [] def get_owner(self, obj): @@ -441,10 +433,8 @@ class ProjectDetailSerializer(ProjectSerializer): return len(obj.members_attr) def get_is_out_of_owner_limits(self, obj): - assert (hasattr(obj, "private_projects_same_owner_attr"), - "instance must have a private_projects_same_owner_attr attribute" - assert (hasattr(obj, "public_projects_same_owner_attr"), - "instance must have a public_projects_same_owner_attr attribute" + assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" + assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" return services.check_if_project_is_out_of_owner_limits( obj, current_memberships=self.get_total_memberships(obj), @@ -453,10 +443,8 @@ class ProjectDetailSerializer(ProjectSerializer): ) def get_is_private_extra_info(self, obj): - assert (hasattr(obj, "private_projects_same_owner_attr"), - "instance must have a private_projects_same_owner_attr attribute" - assert (hasattr(obj, "public_projects_same_owner_attr"), - "instance must have a public_projects_same_owner_attr attribute" + assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" + assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" return services.check_if_project_privacity_can_be_changed( obj, current_memberships=self.get_total_memberships(obj), diff --git a/taiga/routers.py b/taiga/routers.py index d8bc8b00..49db63b2 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -196,11 +196,13 @@ router.register(r"wiki-links", WikiLinkViewSet, # History & Components +from taiga.projects.history.api import EpicHistory from taiga.projects.history.api import UserStoryHistory from taiga.projects.history.api import TaskHistory from taiga.projects.history.api import IssueHistory from taiga.projects.history.api import WikiHistory +router.register(r"history/epic", EpicHistory, base_name="epic-history") router.register(r"history/userstory", UserStoryHistory, base_name="userstory-history") router.register(r"history/task", TaskHistory, base_name="task-history") router.register(r"history/issue", IssueHistory, base_name="issue-history") diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index 3e3bd6f4..2ebe5fa2 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -85,6 +85,7 @@ class TimelineViewSet(ReadOnlyListViewSet): event_type::text = ANY('{issues.issue.change, tasks.task.change, userstories.userstory.change, + epics.epic.change, wiki.wikipage.change}'::text[]) ) """]) @@ -92,6 +93,7 @@ class TimelineViewSet(ReadOnlyListViewSet): qs = qs.exclude(event_type__in=["issues.issue.delete", "tasks.task.delete", "userstories.userstory.delete", + "epics.epic.delete", "wiki.wikipage.delete", "projects.project.change"]) diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index 7b193552..fa951716 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -33,8 +33,8 @@ class TimelineAppConfig(AppConfig): sender=apps.get_model("history", "HistoryEntry"), dispatch_uid="timeline") signals.post_save.connect(handlers.create_membership_push_to_timeline, - sender=apps.get_model("projects", "Membership")) + sender=apps.get_model("projects", "Membership")) signals.pre_delete.connect(handlers.delete_membership_push_to_timeline, - sender=apps.get_model("projects", "Membership")) + sender=apps.get_model("projects", "Membership")) signals.post_save.connect(handlers.create_user_push_to_timeline, - sender=get_user_model()) + sender=get_user_model()) diff --git a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py index 07290281..f3a5fa57 100644 --- a/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py +++ b/taiga/timeline/management/commands/_rebuild_timeline_for_user_creation.py @@ -25,8 +25,9 @@ from django.core.management.base import BaseCommand from django.db.models import Model from django.test.utils import override_settings -from taiga.timeline.service import (_get_impl_key_from_model, - _timeline_impl_map, extract_user_info) +from taiga.timeline.service import _get_impl_key_from_model, +from taiga.timeline.service import _timeline_impl_map, +from taiga.timeline.service import extract_user_info) from taiga.timeline.models import Timeline from taiga.timeline.signals import _push_to_timelines @@ -54,7 +55,8 @@ class BulkCreator(object): bulk_creator = BulkCreator() -def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, + namespace:str="default", extra_data:dict={}): assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model" event_type_key = _get_impl_key_from_model(instance.__class__, event_type) diff --git a/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py index 090cf5f6..6c37b17c 100644 --- a/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py +++ b/taiga/timeline/management/commands/_update_timeline_for_updated_tasks.py @@ -40,7 +40,9 @@ def update_timeline(initial_date, final_date): print("Generating tasks indexed by id dict") task_ids = timelines.values_list("object_id", flat=True) - tasks_per_id = {task.id: task for task in Task.objects.filter(id__in=task_ids).select_related("user_story").iterator()} + + tasks_iterator = Task.objects.filter(id__in=task_ids).select_related("user_story").iterator() + tasks_per_id = {task.id: task for task in tasks_iterator} del task_ids counter = 1 diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py index 947a7418..d3772a4f 100644 --- a/taiga/timeline/management/commands/rebuild_timeline.py +++ b/taiga/timeline/management/commands/rebuild_timeline.py @@ -58,7 +58,8 @@ class BulkCreator(object): bulk_creator = BulkCreator() -def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, + namespace:str="default", extra_data:dict={}): assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model" event_type_key = _get_impl_key_from_model(instance.__class__, event_type) @@ -102,11 +103,13 @@ def generate_timeline(initial_date, final_date, project_id): if project_id: project = Project.objects.get(id=project_id) - us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id", flat=True)] + epic_keys = ['epics.epic:%s'%(id) for id in project.epics.values_list("id", flat=True)] + us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id", + flat=True)] tasks_keys = ['tasks.task:%s'%(id) for id in project.tasks.values_list("id", flat=True)] issue_keys = ['issues.issue:%s'%(id) for id in project.issues.values_list("id", flat=True)] wiki_keys = ['wiki.wikipage:%s'%(id) for id in project.wiki_pages.values_list("id", flat=True)] - keys = us_keys + tasks_keys + issue_keys + wiki_keys + keys = epic_keys + us_keys + tasks_keys + issue_keys + wiki_keys projects = projects.filter(id=project_id) history_entries = history_entries.filter(key__in=keys) @@ -121,7 +124,8 @@ def generate_timeline(initial_date, final_date, project_id): "values_diff": {}, "user": extract_user_info(project.owner), } - _push_to_timelines(project, project.owner, project, "create", project.created_date, extra_data=extra_data) + _push_to_timelines(project, project.owner, project, "create", project.created_date, + extra_data=extra_data) del extra_data for historyEntry in history_entries.iterator(): diff --git a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py index 2f2ae1b5..f4c1a0a4 100644 --- a/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py +++ b/taiga/timeline/management/commands/rebuild_timeline_iterating_per_projects.py @@ -31,7 +31,7 @@ class Command(BaseCommand): total = Project.objects.count() for count,project in enumerate(Project.objects.order_by("id")): - print("""*********************************** - %s/%s %s -***********************************"""%(count+1, total, project.name)) + print("***********************************\n", + " {}/{} {}\n".format(count+1, total, project.name), + "***********************************") call_command("rebuild_timeline", project=project.id) diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index d3e81976..06964eb3 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -52,7 +52,8 @@ def build_project_namespace(project: object): return "{0}:{1}".format("project", project.id) -def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): +def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object, + namespace: str="default", extra_data: dict={}): assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model" from .models import Timeline @@ -74,12 +75,14 @@ def _add_to_object_timeline(obj: object, instance: object, event_type: str, crea ) -def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): +def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, + namespace: str="default", extra_data: dict={}): for obj in objects: _add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data) -def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): +def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, + namespace: str="default", extra_data: dict={}): if isinstance(objects, Model): _add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data) elif isinstance(objects, QuerySet) or isinstance(objects, list): @@ -89,7 +92,8 @@ def _push_to_timeline(objects, instance: object, event_type: str, created_dateti @app.task -def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type, created_datetime, extra_data={}): +def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type, + created_datetime, extra_data={}): ObjModel = apps.get_model(obj_app_label, obj_model_name) try: obj = ObjModel.objects.get(id=obj_id) @@ -156,6 +160,7 @@ def filter_timeline_for_user(timeline, user): content_types = { "view_project": ContentType.objects.get_by_natural_key("projects", "project"), "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"), + "view_epic": ContentType.objects.get_by_natural_key("epics", "epic"), "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"), "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"), "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"), @@ -181,7 +186,8 @@ def filter_timeline_for_user(timeline, user): if membership.is_admin: tl_filter |= Q(project=membership.project) else: - data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions])) + data_content_types = list(filter(None, [content_types.get(a, None) for a in + membership.role.permissions])) data_content_types.append(membership_content_type) tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types) @@ -252,6 +258,14 @@ def extract_milestone_info(instance): } +def extract_epic_info(instance): + return { + "id": instance.pk, + "ref": instance.ref, + "subject": instance.subject, + } + + def extract_userstory_info(instance): return { "id": instance.pk, diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 1bee6c27..7f754b63 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -36,9 +36,23 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d ct = ContentType.objects.get_for_model(obj) if settings.CELERY_ENABLED: - connection.on_commit(lambda: push_to_timelines.delay(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data)) + connection.on_commit(lambda: push_to_timelines.delay(project_id, + user.id, + ct.app_label, + ct.model, + obj.id, + event_type, + created_datetime, + extra_data=extra_data)) else: - push_to_timelines(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data) + push_to_timelines(project_id, + user.id, + ct.app_label, + ct.model, + obj.id, + event_type, + created_datetime, + extra_data=extra_data) def _clean_description_fields(values_diff): diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py index e6065971..9b97fa06 100644 --- a/taiga/timeline/timeline_implementations.py +++ b/taiga/timeline/timeline_implementations.py @@ -43,6 +43,18 @@ def milestone_timeline(instance, extra_data={}): return result +@register_timeline_implementation("epics.epic", "create") +@register_timeline_implementation("epics.epic", "change") +@register_timeline_implementation("epics.epic", "delete") +def epic_timeline(instance, extra_data={}): + result ={ + "epic": service.extract_epic_info(instance), + "project": service.extract_project_info(instance.project), + } + result.update(extra_data) + return result + + @register_timeline_implementation("userstories.userstory", "create") @register_timeline_implementation("userstories.userstory", "change") @register_timeline_implementation("userstories.userstory", "delete") diff --git a/tests/factories.py b/tests/factories.py index b5fdd880..8558907d 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -389,6 +389,16 @@ class IssueTypeFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") +class EpicCustomAttributeFactory(Factory): + class Meta: + model = "custom_attributes.EpicCustomAttribute" + strategy = factory.CREATE_STRATEGY + + name = factory.Sequence(lambda n: "Epic Custom Attribute {}".format(n)) + description = factory.Sequence(lambda n: "Description for Epic Custom Attribute {}".format(n)) + project = factory.SubFactory("tests.factories.ProjectFactory") + + class UserStoryCustomAttributeFactory(Factory): class Meta: model = "custom_attributes.UserStoryCustomAttribute" @@ -419,6 +429,15 @@ class IssueCustomAttributeFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") +class EpicCustomAttributesValuesFactory(Factory): + class Meta: + model = "custom_attributes.EpicCustomAttributesValues" + strategy = factory.CREATE_STRATEGY + + attributes_values = {} + epic = factory.SubFactory("tests.factories.EpicFactory") + + class UserStoryCustomAttributesValuesFactory(Factory): class Meta: model = "custom_attributes.UserStoryCustomAttributesValues" From 46006706c5a95121586199e1bd375259b2b335dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 5 Jul 2016 13:15:51 +0200 Subject: [PATCH 161/261] Add order attr for user stories relationships --- .../projects/epics/migrations/0001_initial.py | 30 ++++++++++++++----- taiga/projects/epics/models.py | 26 ++++++++++++---- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/taiga/projects/epics/migrations/0001_initial.py b/taiga/projects/epics/migrations/0001_initial.py index dc670d7e..78d2dc8f 100644 --- a/taiga/projects/epics/migrations/0001_initial.py +++ b/taiga/projects/epics/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-06-29 14:43 +# Generated by Django 1.9.2 on 2016-07-05 11:12 from __future__ import unicode_literals from django.conf import settings @@ -15,9 +15,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('projects', '0049_auto_20160629_1443'), ('userstories', '0012_auto_20160614_1201'), + ('projects', '0049_auto_20160629_1443'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -30,11 +30,9 @@ class Migration(migrations.Migration): ('is_blocked', models.BooleanField(default=False, verbose_name='is blocked')), ('blocked_note', models.TextField(blank=True, default='', verbose_name='blocked note')), ('ref', models.BigIntegerField(blank=True, db_index=True, default=None, null=True, verbose_name='ref')), - ('is_closed', models.BooleanField(default=False)), ('epics_order', models.IntegerField(default=10000, verbose_name='epics order')), ('created_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created date')), ('modified_date', models.DateTimeField(verbose_name='modified date')), - ('finish_date', models.DateTimeField(blank=True, null=True, verbose_name='finish date')), ('subject', models.TextField(verbose_name='subject')), ('description', models.TextField(blank=True, verbose_name='description')), ('client_requirement', models.BooleanField(default=False, verbose_name='is client requirement')), @@ -43,15 +41,33 @@ class Migration(migrations.Migration): ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owned_epics', to=settings.AUTH_USER_MODEL, verbose_name='owner')), ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epics', to='projects.Project', verbose_name='project')), ('status', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='epics', to='projects.EpicStatus', verbose_name='status')), - ('user_stories', models.ManyToManyField(related_name='epics', to='userstories.UserStory', verbose_name='user stories')), ], options={ + 'ordering': ['project', 'epics_order', 'ref'], 'verbose_name_plural': 'epics', - 'ordering': ['project', 'ref'], 'verbose_name': 'epic', }, bases=(taiga.projects.notifications.mixins.WatchedModelMixin, models.Model), ), + migrations.CreateModel( + name='RelatedUserStory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField(default=10000, verbose_name='order')), + ('epic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epics.Epic')), + ('user_story', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='userstories.UserStory')), + ], + options={ + 'ordering': ['user_story', 'order', 'id'], + 'verbose_name_plural': 'related user stories', + 'verbose_name': 'related user story', + }, + ), + migrations.AddField( + model_name='epic', + name='user_stories', + field=models.ManyToManyField(related_name='epics', through='epics.RelatedUserStory', to='userstories.UserStory', verbose_name='user stories'), + ), # Execute trigger after epic update migrations.RunSQL( """ diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index b4a9118a..d55c1dff 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -39,8 +39,6 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M status = models.ForeignKey("projects.EpicStatus", null=True, blank=True, related_name="epics", verbose_name=_("status"), on_delete=models.SET_NULL) - is_closed = models.BooleanField(default=False) - epics_order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("epics order")) @@ -49,8 +47,6 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M default=timezone.now) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) - finish_date = models.DateTimeField(null=True, blank=True, - verbose_name=_("finish date")) subject = models.TextField(null=False, blank=False, verbose_name=_("subject")) @@ -64,6 +60,7 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M verbose_name=_("is team requirement")) user_stories = models.ManyToManyField("userstories.UserStory", related_name="epics", + through='RelatedUserStory', verbose_name=_("user stories")) attachments = GenericRelation("attachments.Attachment") @@ -73,7 +70,7 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M class Meta: verbose_name = "epic" verbose_name_plural = "epics" - ordering = ["project", "ref"] + ordering = ["project", "epics_order", "ref"] def save(self, *args, **kwargs): if not self._importing or not self.modified_date: @@ -85,7 +82,24 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M super().save(*args, **kwargs) def __str__(self): - return "({1}) {0}".format(self.ref, self.subject) + return "#{0} {1}".format(self.ref, self.subject) def __repr__(self): return "" % (self.id) + + + +class RelatedUserStory(models.Model): + user_story = models.ForeignKey("userstories.UserStory", on_delete=models.CASCADE) + epic = models.ForeignKey("epics.Epic", on_delete=models.CASCADE) + + order = models.IntegerField(null=False, blank=False, default=10000, + verbose_name=_("order")) + + class Meta: + verbose_name = "related user story" + verbose_name_plural = "related user stories" + ordering = ["user_story", "order", "id"] + + def __str__(self): + return "{0} - {1}".format(self.epic, self.user_story) From 6bdfe255038884004fd9919aea7cabaa8227b09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 5 Jul 2016 13:16:28 +0200 Subject: [PATCH 162/261] Save related stories in history items for epics --- taiga/projects/history/freeze_impl.py | 15 +++++++++++++-- taiga/projects/history/models.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 7d5b3543..1c2fae44 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -187,6 +187,18 @@ def _generic_extract(obj:object, fields:list, default=None) -> dict: return result +@as_tuple +def extract_user_stories(obj) -> list: + for user_story in obj.user_stories.all(): + + yield {"id": user_story.id, + "ref": user_story.ref, + "subject": user_story.subject, + "project": { + "id": user_story.project.id, + "name": user_story.project.name, + "slug": user_story.project.slug}} + @as_tuple def extract_attachments(obj) -> list: for attach in obj.attachments.all(): @@ -285,8 +297,6 @@ def epic_freezer(epic) -> dict: "ref": epic.ref, "owner": epic.owner_id, "status": epic.status.id if epic.status else None, - "is_closed": epic.is_closed, - "finish_date": str(epic.finish_date), "epics_order": epic.epics_order, "subject": epic.subject, "description": epic.description, @@ -300,6 +310,7 @@ def epic_freezer(epic) -> dict: "blocked_note": epic.blocked_note, "blocked_note_html": mdrender(epic.project, epic.blocked_note), "custom_attributes": extract_epic_custom_attributes(epic), + "user_stories": extract_user_stories(epic), } return snapshot diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index d5c023a9..aafaa915 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -258,6 +258,24 @@ class HistoryEntry(models.Model): if custom_attributes["new"] or custom_attributes["changed"] or custom_attributes["deleted"]: value = custom_attributes + elif key == "user_stories": + user_stories = { + "new": [], + "deleted": [], + } + + olduss = {x["id"]:x for x in self.diff["user_stories"][0]} + newuss = {x["id"]:x for x in self.diff["user_stories"][1]} + + for usid in set(tuple(olduss.keys()) + tuple(newuss.keys())): + if usid in olduss and usid not in newuss: + user_stories["deleted"].append(olduss[usid]) + elif usid not in olduss and usid in newuss: + user_stories["new"].append(newuss[usid]) + + if user_stories["new"] or user_stories["changed"] or user_stories["deleted"]: + value = user_stories + elif key in self.values: value = [resolve_value(key, x) for x in self.diff[key]] else: From 329a3e5ef33a4f25362038f86648f6d7a0ef9ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 7 Jul 2016 11:14:03 +0200 Subject: [PATCH 163/261] Add default permissions --- taiga/projects/epics/permissions.py | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 taiga/projects/epics/permissions.py diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py new file mode 100644 index 00000000..86f2626e --- /dev/null +++ b/taiga/projects/epics/permissions.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser +from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin + +from taiga.permissions.permissions import CommentAndOrUpdatePerm + + +class EpicPermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_epics') + create_perms = HasProjectPerm('add_epic') + update_perms = CommentAndOrUpdatePerm('modify_epic', 'comment_epic') + partial_update_perms = CommentAndOrUpdatePerm('modify_epic', 'comment_epic') + destroy_perms = HasProjectPerm('delete_epic') + list_perms = AllowAny() + filters_data_perms = AllowAny() + csv_perms = AllowAny() + bulk_create_perms = HasProjectPerm('add_epic') + bulk_update_order_perms = HasProjectPerm('modify_epic') + upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') + downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') + watch_perms = IsAuthenticated() & HasProjectPerm('view_epics') + unwatch_perms = IsAuthenticated() & HasProjectPerm('view_epics') + + +class EpicVotersPermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_epics') + list_perms = HasProjectPerm('view_epics') + + +class EpicWatchersPermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_epics') + list_perms = HasProjectPerm('view_epics') From d5b2bc95ab9e097da0ef71200bf6e867dab966fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 7 Jul 2016 12:27:42 +0200 Subject: [PATCH 164/261] Add initial Epic viewset (+ Voters and watchers) --- taiga/base/filters.py | 4 + taiga/projects/epics/api.py | 223 +++++++++++++++++ taiga/projects/epics/serializers.py | 74 ++++++ taiga/projects/epics/services.py | 376 ++++++++++++++++++++++++++++ taiga/projects/epics/utils.py | 39 +++ taiga/projects/epics/validators.py | 67 +++++ taiga/routers.py | 11 + 7 files changed, 794 insertions(+) create mode 100644 taiga/projects/epics/api.py create mode 100644 taiga/projects/epics/serializers.py create mode 100644 taiga/projects/epics/services.py create mode 100644 taiga/projects/epics/utils.py create mode 100644 taiga/projects/epics/validators.py diff --git a/taiga/base/filters.py b/taiga/base/filters.py index bddec10e..187033c1 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -160,6 +160,10 @@ class CanViewProjectFilterBackend(PermissionBasedFilterBackend): permission = "view_project" +class CanViewEpicsFilterBackend(PermissionBasedFilterBackend): + permission = "view_epics" + + class CanViewUsFilterBackend(PermissionBasedFilterBackend): permission = "view_us" diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py new file mode 100644 index 00000000..7b703ba5 --- /dev/null +++ b/taiga/projects/epics/api.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.http import HttpResponse +from django.utils.translation import ugettext as _ + +from taiga.base.api.utils import get_object_or_404 +from taiga.base import filters, response +from taiga.base import exceptions as exc +from taiga.base.decorators import list_route +from taiga.base.api import ModelCrudViewSet, ModelListViewSet +from taiga.base.api.mixins import BlockedByProjectMixin + +from taiga.projects.history.mixins import HistoryResourceMixin +from taiga.projects.models import Project, EpicStatus +from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin +from taiga.projects.occ import OCCResourceMixin +from taiga.projects.tagging.api import TaggedResourceMixin +from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin + +from . import models +from . import permissions +from . import serializers +from . import services +from . import validators +from . import utils as epics_utils + + +class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, + WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, + ModelCrudViewSet): + validator_class = validators.EpicValidator + queryset = models.Epic.objects.all() + permission_classes = (permissions.EpicPermission,) + filter_backends = (filters.CanViewEpicsFilterBackend, + filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.WatchersFilter, + filters.QFilter) + retrieve_exclude_filters = (filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.WatchersFilter) + filter_fields = ["project", + "project__slug", + "assigned_to", + "status__is_closed"] + + def get_serializer_class(self, *args, **kwargs): + if self.action in ["retrieve", "by_ref"]: + return serializers.EpicNeighborsSerializer + + if self.action == "list": + return serializers.EpicListSerializer + + return serializers.EpicSerializer + + def get_queryset(self): + qs = super().get_queryset() + qs = qs.select_related("project", + "status", + "owner", + "assigned_to") + + include_attachments = "include_attachments" in self.request.QUERY_PARAMS + qs = epics_utils.attach_extra_info(qs, user=self.request.user, + include_attachments=include_attachments) + + return qs + + def pre_conditions_on_save(self, obj): + super().pre_conditions_on_save(obj) + + if obj.status and obj.status.project != obj.project: + raise exc.WrongArguments(_("You don't have permissions to set this status to this epic.")) + + def pre_save(self, obj): + if not obj.id: + obj.owner = self.request.user + super().pre_save(obj) + + def update(self, request, *args, **kwargs): + self.object = self.get_object_or_none() + project_id = request.DATA.get('project', None) + if project_id and self.object and self.object.project.id != project_id: + try: + new_project = Project.objects.get(pk=project_id) + self.check_permissions(request, "destroy", self.object) + self.check_permissions(request, "create", new_project) + + status_id = request.DATA.get('status', None) + if status_id is not None: + try: + old_status = self.object.project.epic_statuses.get(pk=status_id) + new_status = new_project.epic_statuses.get(slug=old_status.slug) + request.DATA['status'] = new_status.id + except EpicStatus.DoesNotExist: + request.DATA['status'] = new_project.default_epic_status.id + + except Project.DoesNotExist: + return response.BadRequest(_("The project doesn't exist")) + + return super().update(request, *args, **kwargs) + + @list_route(methods=["GET"]) + def filters_data(self, request, *args, **kwargs): + project_id = request.QUERY_PARAMS.get("project", None) + project = get_object_or_404(Project, id=project_id) + + filter_backends = self.get_filter_backends() + statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) + assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) + owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) + + queryset = self.get_queryset() + querysets = { + "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), + "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), + "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), + "tags": self.filter_queryset(queryset) + } + return response.Ok(services.get_epics_filters_data(project, querysets)) + + @list_route(methods=["GET"]) + def by_ref(self, request): + retrieve_kwargs = { + "ref": request.QUERY_PARAMS.get("ref", None) + } + project_id = request.QUERY_PARAMS.get("project", None) + if project_id is not None: + retrieve_kwargs["project_id"] = project_id + + project_slug = request.QUERY_PARAMS.get("project__slug", None) + if project_slug is not None: + retrieve_kwargs["project__slug"] = project_slug + + return self.retrieve(request, **retrieve_kwargs) + + #@list_route(methods=["GET"]) + #def csv(self, request): + # uuid = request.QUERY_PARAMS.get("uuid", None) + # if uuid is None: + # return response.NotFound() + + # project = get_object_or_404(Project, epics_csv_uuid=uuid) + # queryset = project.epics.all().order_by('ref') + # data = services.epics_to_csv(project, queryset) + # csv_response = HttpResponse(data.getvalue(), content_type='application/csv; charset=utf-8') + # csv_response['Content-Disposition'] = 'attachment; filename="epics.csv"' + # return csv_response + + @list_route(methods=["POST"]) + def bulk_create(self, request, **kwargs): + validator = validators.EpicsBulkValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data + project = Project.objects.get(id=data["project_id"]) + self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + epics = services.create_epics_in_bulk( + data["bulk_epics"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], + status_id=data.get("status_id") or project.default_epic_status_id, + project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) + + epics = self.get_queryset().filter(id__in=[i.id for i in epics]) + epics_serialized = self.get_serializer_class()(epics, many=True) + + return response.Ok(epics_serialized.data) + + return response.BadRequest(validator.errors) + + def _bulk_update_order(self, order_field, request, **kwargs): + validator = validators.UpdateEpicsOrderBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) + + data = validator.data + project = get_object_or_404(Project, pk=data["project_id"]) + + self.check_permissions(request, "bulk_update_order", project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + services.update_epics_order_in_bulk(data["bulk_epics"], + project=project, + field=order_field) + services.snapshot_epics_in_bulk(data["bulk_epics"], request.user) + + return response.NoContent() + + @list_route(methods=["POST"]) + def bulk_update_epic_order(self, request, **kwargs): + return self._bulk_update_order("epic_order", request, **kwargs) + + +class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.EpicVotersPermission,) + resource_model = models.Epic + + +class EpicWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): + permission_classes = (permissions.EpicWatchersPermission,) + resource_model = models.Epic diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py new file mode 100644 index 00000000..d9d45b2b --- /dev/null +++ b/taiga/projects/epics/serializers.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base.api import serializers +from taiga.base.fields import Field, MethodField +from taiga.base.neighbors import NeighborsSerializerMixin + +from taiga.mdrender.service import render as mdrender +from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer +from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin + + +class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, + serializers.LightSerializer): + + id = Field() + ref = Field() + project = Field(attr="project_id") + created_date = Field() + modified_date = Field() + subject = Field() + epic_order = Field() + client_requirement = Field() + team_requirement = Field() + version = Field() + watchers = Field() + is_blocked = Field() + blocked_note = Field() + tags = Field() + is_closed = MethodField() + + def get_is_closed(self, obj): + return obj.status is not None and obj.status.is_closed + + +class EpicSerializer(EpicListSerializer): + comment = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() + + def get_comment(self, obj): + return "" + + def get_blocked_note_html(self, obj): + return mdrender(obj.project, obj.blocked_note) + + def get_description_html(self, obj): + return mdrender(obj.project, obj.description) + + +class EpicNeighborsSerializer(NeighborsSerializerMixin, EpicSerializer): + pass diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py new file mode 100644 index 00000000..01a0f0fa --- /dev/null +++ b/taiga/projects/epics/services.py @@ -0,0 +1,376 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import csv +import io +from collections import OrderedDict +from operator import itemgetter +from contextlib import closing + +from django.db import connection +from django.utils.translation import ugettext as _ + +from taiga.base.utils import db, text +from taiga.projects.history.services import take_snapshot +from taiga.projects.epics.apps import connect_epics_signals +from taiga.projects.epics.apps import disconnect_epics_signals +from taiga.events import events +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.notifications.utils import attach_watchers_to_queryset + +from . import models + + +##################################################### +# Bulk actions +##################################################### + +def get_epics_from_bulk(bulk_data, **additional_fields): + """Convert `bulk_data` into a list of epics. + + :param bulk_data: List of epics in bulk format. + :param additional_fields: Additional fields when instantiating each epic. + + :return: List of `Epic` instances. + """ + return [models.Epic(subject=line, **additional_fields) + for line in text.split_in_lines(bulk_data)] + + +def create_epics_in_bulk(bulk_data, callback=None, precall=None, **additional_fields): + """Create epics from `bulk_data`. + + :param bulk_data: List of epics in bulk format. + :param callback: Callback to execute after each epic save. + :param additional_fields: Additional fields when instantiating each epic. + + :return: List of created `Epic` instances. + """ + epics = get_epics_from_bulk(bulk_data, **additional_fields) + + disconnect_epics_signals() + + try: + db.save_in_bulk(epics, callback, precall) + finally: + connect_epics_signals() + + return epics + + +def update_epics_order_in_bulk(bulk_data: list, field: str, project: object): + """ + Update the order of some epics. + `bulk_data` should be a list of tuples with the following format: + + [(, {: , ...}), ...] + """ + epic_ids = [] + new_order_values = [] + for epic_data in bulk_data: + epic_ids.append(epic_data["epic_id"]) + new_order_values.append({field: epic_data["order"]}) + + events.emit_event_for_ids(ids=epic_ids, + content_type="epics.epic", + projectid=project.pk) + + db.update_in_bulk_with_ids(epic_ids, new_order_values, model=models.Epic) + + +def snapshot_epics_in_bulk(bulk_data, user): + for epic_data in bulk_data: + try: + epic = models.Epic.objects.get(pk=epic_data['epic_id']) + take_snapshot(epic, user=user) + except models.Epic.DoesNotExist: + pass + + +##################################################### +# CSV +##################################################### +# +#def epics_to_csv(project, queryset): +# csv_data = io.StringIO() +# fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start", +# "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", +# "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order", +# "epicboard_order", "attachments", "external_reference", "tags", "watchers", "voters", +# "created_date", "modified_date", "finished_date"] +# +# custom_attrs = project.epiccustomattributes.all() +# for custom_attr in custom_attrs: +# fieldnames.append(custom_attr.name) +# +# queryset = queryset.prefetch_related("attachments", +# "custom_attributes_values") +# queryset = queryset.select_related("milestone", +# "owner", +# "assigned_to", +# "status", +# "project") +# +# queryset = attach_total_voters_to_queryset(queryset) +# queryset = attach_watchers_to_queryset(queryset) +# +# writer = csv.DictWriter(csv_data, fieldnames=fieldnames) +# writer.writeheader() +# for epic in queryset: +# epic_data = { +# "ref": epic.ref, +# "subject": epic.subject, +# "description": epic.description, +# "user_story": epic.user_story.ref if epic.user_story else None, +# "sprint": epic.milestone.name if epic.milestone else None, +# "sprint_estimated_start": epic.milestone.estimated_start if epic.milestone else None, +# "sprint_estimated_finish": epic.milestone.estimated_finish if epic.milestone else None, +# "owner": epic.owner.username if epic.owner else None, +# "owner_full_name": epic.owner.get_full_name() if epic.owner else None, +# "assigned_to": epic.assigned_to.username if epic.assigned_to else None, +# "assigned_to_full_name": epic.assigned_to.get_full_name() if epic.assigned_to else None, +# "status": epic.status.name if epic.status else None, +# "is_iocaine": epic.is_iocaine, +# "is_closed": epic.status is not None and epic.status.is_closed, +# "us_order": epic.us_order, +# "epicboard_order": epic.epicboard_order, +# "attachments": epic.attachments.count(), +# "external_reference": epic.external_reference, +# "tags": ",".join(epic.tags or []), +# "watchers": epic.watchers, +# "voters": epic.total_voters, +# "created_date": epic.created_date, +# "modified_date": epic.modified_date, +# "finished_date": epic.finished_date, +# } +# for custom_attr in custom_attrs: +# value = epic.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) +# epic_data[custom_attr.name] = value +# +# writer.writerow(epic_data) +# +# return csv_data + + +##################################################### +# Api filter data +##################################################### + +def _get_epics_statuses(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + SELECT "projects_epicstatus"."id", + "projects_epicstatus"."name", + "projects_epicstatus"."color", + "projects_epicstatus"."order", + (SELECT count(*) + FROM "epics_epic" + INNER JOIN "projects_project" ON + ("epics_epic"."project_id" = "projects_project"."id") + WHERE {where} AND "epics_epic"."status_id" = "projects_epicstatus"."id") + FROM "projects_epicstatus" + WHERE "projects_epicstatus"."project_id" = %s + ORDER BY "projects_epicstatus"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_epics_assigned_to(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH counters AS ( + SELECT assigned_to_id, count(assigned_to_id) count + FROM "epics_epic" + INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id") + WHERE {where} AND "epics_epic"."assigned_to_id" IS NOT NULL + GROUP BY assigned_to_id + ) + + SELECT "projects_membership"."user_id" user_id, + "users_user"."full_name", + "users_user"."username", + COALESCE("counters".count, 0) count + FROM projects_membership + LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id") + INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL + + -- unassigned epics + UNION + + SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count + FROM "epics_epic" + INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id") + WHERE {where} AND "epics_epic"."assigned_to_id" IS NULL + GROUP BY assigned_to_id + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id] + where_params) + rows = cursor.fetchall() + + result = [] + none_valued_added = False + for id, full_name, username, count in rows: + result.append({ + "id": id, + "full_name": full_name or username or "", + "count": count, + }) + + if id is None: + none_valued_added = True + + # If there was no epic with null assigned_to we manually add it + if not none_valued_added: + result.append({ + "id": None, + "full_name": "", + "count": 0, + }) + + return sorted(result, key=itemgetter("full_name")) + + +def _get_epics_owners(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH counters AS ( + SELECT "epics_epic"."owner_id" owner_id, + count(coalesce("epics_epic"."owner_id", -1)) count + FROM "epics_epic" + INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id") + WHERE {where} + GROUP BY "epics_epic"."owner_id" + ) + + SELECT "projects_membership"."user_id" id, + "users_user"."full_name", + "users_user"."username", + COALESCE("counters".count, 0) count + FROM projects_membership + LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") + INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL + + -- System users + UNION + + SELECT "users_user"."id" user_id, + "users_user"."full_name" full_name, + "users_user"."username" username, + COALESCE("counters".count, 0) count + FROM users_user + LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id") + WHERE ("users_user"."is_system" IS TRUE) + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, username, count in rows: + if count > 0: + result.append({ + "id": id, + "full_name": full_name or username or "", + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_epics_tags(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH epics_tags AS ( + SELECT tag, + COUNT(tag) counter FROM ( + SELECT UNNEST(epics_epic.tags) tag + FROM epics_epic + INNER JOIN projects_project + ON (epics_epic.project_id = projects_project.id) + WHERE {where}) tags + GROUP BY tag), + project_tags AS ( + SELECT reduce_dim(tags_colors) tag_color + FROM projects_project + WHERE id=%s) + + SELECT tag_color[1] tag, COALESCE(epics_tags.counter, 0) counter + FROM project_tags + LEFT JOIN epics_tags ON project_tags.tag_color[1] = epics_tags.tag + ORDER BY tag + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for name, count in rows: + result.append({ + "name": name, + "count": count, + }) + return sorted(result, key=itemgetter("name")) + + +def get_epics_filters_data(project, querysets): + """ + Given a project and an epics queryset, return a simple data structure + of all possible filters for the epics in the queryset. + """ + data = OrderedDict([ + ("statuses", _get_epics_statuses(project, querysets["statuses"])), + ("assigned_to", _get_epics_assigned_to(project, querysets["assigned_to"])), + ("owners", _get_epics_owners(project, querysets["owners"])), + ("tags", _get_epics_tags(project, querysets["tags"])), + ]) + + return data diff --git a/taiga/projects/epics/utils.py b/taiga/projects/epics/utils.py new file mode 100644 index 00000000..d10dddab --- /dev/null +++ b/taiga/projects/epics/utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 taiga.projects.attachments.utils import attach_basic_attachments +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False): + + if include_attachments: + queryset = attach_basic_attachments(queryset) + queryset = queryset.extra(select={"include_attachments": "True"}) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py new file mode 100644 index 00000000..9d7a617f --- /dev/null +++ b/taiga/projects/epics/validators.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 as _ + +from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.exceptions import ValidationError +from taiga.base.fields import PgArrayField +from taiga.projects.milestones.validators import MilestoneExistsValidator +from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.validators import ProjectExistsValidator +from . import models + + +class EpicExistsValidator: + def validate_epic_id(self, attrs, source): + value = attrs[source] + if not models.Epic.objects.filter(pk=value).exists(): + msg = _("There's no epic with that id") + raise ValidationError(msg) + return attrs + + +class EpicValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): + tags = TagsAndTagsColorsField(default=[], required=False) + external_reference = PgArrayField(required=False) + + class Meta: + model = models.Epic + read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + + +class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator, + validators.Validator): + project_id = serializers.IntegerField() + status_id = serializers.IntegerField(required=False) + bulk_epics = serializers.CharField() + + +# Order bulk validators + +class _EpicOrderBulkValidator(EpicExistsValidator, validators.Validator): + epic_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateEpicsOrderBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_epics = _EpicOrderBulkValidator(many=True) diff --git a/taiga/routers.py b/taiga/routers.py index 49db63b2..e0d0baa1 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -145,6 +145,10 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet, from taiga.projects.milestones.api import MilestoneViewSet from taiga.projects.milestones.api import MilestoneWatchersViewSet +from taiga.projects.epics.api import EpicViewSet +from taiga.projects.epics.api import EpicVotersViewSet +from taiga.projects.epics.api import EpicWatchersViewSet + from taiga.projects.userstories.api import UserStoryViewSet from taiga.projects.userstories.api import UserStoryVotersViewSet from taiga.projects.userstories.api import UserStoryWatchersViewSet @@ -166,6 +170,13 @@ router.register(r"milestones", MilestoneViewSet, router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers") +router.register(r"epics", EpicViewSet, + base_name="epics") +router.register(r"epics/(?P\d+)/voters", EpicVotersViewSet, + base_name="epic-voters") +router.register(r"epics/(?P\d+)/watchers", EpicWatchersViewSet, + base_name="epic-watchers") + router.register(r"userstories", UserStoryViewSet, base_name="userstories") router.register(r"userstories/(?P\d+)/voters", UserStoryVotersViewSet, From 82bff6f5bfb9d500c6d3b656fac2812a3cbad25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 7 Jul 2016 13:23:48 +0200 Subject: [PATCH 165/261] Fixup. Custom atributes --- .../test_epics_custom_attributes_resource.py | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 tests/integration/resources_permissions/test_epics_custom_attributes_resource.py diff --git a/tests/integration/resources_permissions/test_epics_custom_attributes_resource.py b/tests/integration/resources_permissions/test_epics_custom_attributes_resource.py new file mode 100644 index 00000000..c35b93bd --- /dev/null +++ b/tests/integration/resources_permissions/test_epics_custom_attributes_resource.py @@ -0,0 +1,458 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects import choices as project_choices +from taiga.projects.custom_attributes import serializers +from taiga.permissions.choices import (MEMBERS_PERMISSIONS, + ANON_PERMISSIONS) + +from tests import factories as f +from tests.utils import helper_test_http_method + +import pytest +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + m.superuser = f.UserFactory.create(is_superuser=True) + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner) + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner) + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + blocked_code=project_choices.BLOCKED_BY_STAFF) + + m.public_membership = f.MembershipFactory(project=m.public_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory(project=m.private_project1, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + f.MembershipFactory(project=m.private_project1, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project1, + role__permissions=[]) + + m.private_membership2 = f.MembershipFactory(project=m.private_project2, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.private_project2, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.private_project2, + role__permissions=[]) + + m.blocked_membership = f.MembershipFactory(project=m.blocked_project, + user=m.project_member_with_perms, + email=m.project_member_with_perms.email, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + email=m.project_member_without_perms.email, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + m.public_epic_ca = f.EpicCustomAttributeFactory(project=m.public_project) + m.private_epic_ca1 = f.EpicCustomAttributeFactory(project=m.private_project1) + m.private_epic_ca2 = f.EpicCustomAttributeFactory(project=m.private_project2) + m.blocked_epic_ca = f.EpicCustomAttributeFactory(project=m.blocked_project) + + m.public_epic = f.EpicFactory(project=m.public_project, + status__project=m.public_project) + m.private_epic1 = f.EpicFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_epic2 = f.EpicFactory(project=m.private_project2, + status__project=m.private_project2) + m.blocked_epic = f.EpicFactory(project=m.blocked_project, + status__project=m.blocked_project) + + m.public_epic_cav = m.public_epic.custom_attributes_values + m.private_epic_cav1 = m.private_epic1.custom_attributes_values + m.private_epic_cav2 = m.private_epic2.custom_attributes_values + m.blocked_epic_cav = m.blocked_epic.custom_attributes_values + + return m + + +######################################################### +# Epic Custom Attribute +######################################################### + +def test_epic_custom_attribute_retrieve(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private1_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private2_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_custom_attribute_create(client, data): + public_url = reverse('epic-custom-attributes-list') + private1_url = reverse('epic-custom-attributes-list') + private2_url = reverse('epic-custom-attributes-list') + blocked_url = reverse('epic-custom-attributes-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_ca_data = {"name": "test-new", "project": data.public_project.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', public_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + epic_ca_data = {"name": "test-new", "project": data.private_project1.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', private1_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + epic_ca_data = {"name": "test-new", "project": data.private_project2.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', private2_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 201] + + epic_ca_data = {"name": "test-new", "project": data.blocked_project.id} + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'post', blocked_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_custom_attribute_update(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.public_epic_ca).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', public_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.private_epic_ca1).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', private1_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.private_epic_ca2).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', private2_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 200] + + epic_ca_data = serializers.EpicCustomAttributeSerializer(data.blocked_epic_ca).data + epic_ca_data["name"] = "test" + epic_ca_data = json.dumps(epic_ca_data) + results = helper_test_http_method(client, 'put', private2_url, epic_ca_data, users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_custom_attribute_delete(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private1_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private2_url, None, users) + assert results == [401, 403, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 403, 451] + + + +def test_epic_custom_attribute_list(client, data): + url = reverse('epic-custom-attributes-list') + + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_without_perms) + response = client.json.get(url) + assert len(response.data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + response = client.json.get(url) + assert len(response.data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + response = client.json.get(url) + assert len(response.data) == 4 + assert response.status_code == 200 + + +def test_epic_custom_attribute_patch(client, data): + public_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.public_epic_ca.pk}) + private1_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca1.pk}) + private2_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.private_epic_ca2.pk}) + blocked_url = reverse('epic-custom-attributes-detail', kwargs={"pk": data.blocked_epic_ca.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 200] + results = helper_test_http_method(client, 'patch', blocked_url, '{"name": "Test"}', users) + assert results == [401, 403, 403, 403, 451] + + +def test_epic_custom_attribute_action_bulk_update_order(client, data): + url = reverse('epic-custom-attributes-bulk-update-order') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.public_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.private_project1.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.private_project2.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 204] + + post_data = json.dumps({ + "bulk_epic_custom_attributes": [(1,2)], + "project": data.blocked_project.pk + }) + results = helper_test_http_method(client, 'post', url, post_data, users) + assert results == [401, 403, 403, 403, 451] + +######################################################### +# Epic Custom Attribute +######################################################### + + +def test_epic_custom_attributes_values_retrieve(client, data): + public_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.public_epic.pk}) + private_url1 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic1.pk}) + private_url2 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic2.pk}) + blocked_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_custom_attributes_values_update(client, data): + public_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.public_epic.pk}) + private_url1 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic1.pk}) + private_url2 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic2.pk}) + blocked_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.public_epic_cav).data + epic_data["attributes_values"] = {str(data.public_epic_ca.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.private_epic_cav1).data + epic_data["attributes_values"] = {str(data.private_epic_ca1.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.private_epic_cav2).data + epic_data["attributes_values"] = {str(data.private_epic_ca2.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = serializers.EpicCustomAttributesValuesSerializer(data.blocked_epic_cav).data + epic_data["attributes_values"] = {str(data.blocked_epic_ca.pk): "test"} + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_custom_attributes_values_patch(client, data): + public_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.public_epic.pk}) + private_url1 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic1.pk}) + private_url2 = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.private_epic2.pk}) + blocked_url = reverse('epic-custom-attributes-values-detail', kwargs={"epic_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"attributes_values": {str(data.public_epic_ca.pk): "test"}, + "version": data.public_epic.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_epic_ca1.pk): "test"}, + "version": data.private_epic1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.private_epic_ca2.pk): "test"}, + "version": data.private_epic2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"attributes_values": {str(data.blocked_epic_ca.pk): "test"}, + "version": data.blocked_epic.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] From 7a412fb5367879b748098094d44d4768cf00e740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 7 Jul 2016 15:22:34 +0200 Subject: [PATCH 166/261] Fix some tests --- taiga/base/filters.py | 2 +- taiga/permissions/choices.py | 4 +- taiga/projects/attachments/permissions.py | 2 +- taiga/projects/epics/api.py | 10 +- taiga/projects/epics/apps.py | 2 +- taiga/projects/epics/serializers.py | 2 +- .../fixtures/initial_project_templates.json | 44 +- .../migrations/0049_auto_20160629_1443.py | 4 +- taiga/timeline/service.py | 2 +- .../migrations/0022_auto_20160629_1443.py | 2 +- .../test_epics_resources.py | 901 ++++++++++++++++++ 11 files changed, 939 insertions(+), 36 deletions(-) create mode 100644 tests/integration/resources_permissions/test_epics_resources.py diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 187033c1..a30e2dcf 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -203,7 +203,7 @@ class PermissionBasedAttachmentFilterBackend(PermissionBasedFilterBackend): class CanViewEpicAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): - permission = "view_epic" + permission = "view_epics" class CanViewUserStoryAttachmentFilterBackend(PermissionBasedAttachmentFilterBackend): diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py index 5cda50a5..3564acc8 100644 --- a/taiga/permissions/choices.py +++ b/taiga/permissions/choices.py @@ -22,7 +22,7 @@ from django.utils.translation import ugettext_lazy as _ ANON_PERMISSIONS = [ ('view_project', _('View project')), ('view_milestones', _('View milestones')), - ('view_epic', _('View epic')), + ('view_epics', _('View epic')), ('view_us', _('View user stories')), ('view_tasks', _('View tasks')), ('view_issues', _('View issues')), @@ -38,7 +38,7 @@ MEMBERS_PERMISSIONS = [ ('modify_milestone', _('Modify milestone')), ('delete_milestone', _('Delete milestone')), # US permissions - ('view_epic', _('View epic')), + ('view_epics', _('View epic')), ('add_epic', _('Add epic')), ('modify_epic', _('Modify epic')), ('comment_epic', _('Comment epic')), diff --git a/taiga/projects/attachments/permissions.py b/taiga/projects/attachments/permissions.py index ee768014..4c7a7915 100644 --- a/taiga/projects/attachments/permissions.py +++ b/taiga/projects/attachments/permissions.py @@ -29,7 +29,7 @@ class IsAttachmentOwnerPerm(PermissionComponent): class EpicAttachmentPermission(TaigaResourcePermission): - retrieve_perms = HasProjectPerm('view_epic') | IsAttachmentOwnerPerm() + retrieve_perms = HasProjectPerm('view_epics') | IsAttachmentOwnerPerm() create_perms = HasProjectPerm('modify_epic') update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm() partial_update_perms = HasProjectPerm('modify_epic') | IsAttachmentOwnerPerm() diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 7b703ba5..e71106d1 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -178,9 +178,11 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, raise exc.Blocked(_("Blocked element")) epics = services.create_epics_in_bulk( - data["bulk_epics"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], + data["bulk_epics"], status_id=data.get("status_id") or project.default_epic_status_id, - project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) + project=project, + owner=request.user, + callback=self.post_save, precall=self.pre_save) epics = self.get_queryset().filter(id__in=[i.id for i in epics]) epics_serialized = self.get_serializer_class()(epics, many=True) @@ -209,8 +211,8 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.NoContent() @list_route(methods=["POST"]) - def bulk_update_epic_order(self, request, **kwargs): - return self._bulk_update_order("epic_order", request, **kwargs) + def bulk_update_epics_order(self, request, **kwargs): + return self._bulk_update_order("epics_order", request, **kwargs) class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): diff --git a/taiga/projects/epics/apps.py b/taiga/projects/epics/apps.py index 5389b98a..bf489ea0 100644 --- a/taiga/projects/epics/apps.py +++ b/taiga/projects/epics/apps.py @@ -43,7 +43,7 @@ def connect_all_epics_signals(): def disconnect_epics_signals(): - signals.pre_save.disconnect(sender=apps.get_model("epics", "Task"), + signals.pre_save.disconnect(sender=apps.get_model("epics", "Epic"), dispatch_uid="tags_normalization") diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py index d9d45b2b..4118553e 100644 --- a/taiga/projects/epics/serializers.py +++ b/taiga/projects/epics/serializers.py @@ -40,7 +40,7 @@ class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, created_date = Field() modified_date = Field() subject = Field() - epic_order = Field() + epics_order = Field() client_requirement = Field() team_requirement = Field() version = Field() diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index 83d64b80..5c711c00 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -8,7 +8,7 @@ "description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers", "order": 1, "created_date": "2014-04-22T14:48:43.596Z", - "modified_date": "2016-06-29T14:52:11.273Z", + "modified_date": "2016-07-07T13:18:25.350Z", "default_owner_role": "product-owner", "is_epics_activated": true, "is_backlog_activated": true, @@ -17,16 +17,16 @@ "is_issues_activated": true, "videoconferences": null, "videoconferences_extra_data": "", - "default_options": "{\"epic_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"us_status\": \"New\", \"points\": \"?\", \"priority\": \"Normal\", \"task_status\": \"New\", \"issue_status\": \"New\"}", - "epic_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"order\": 5, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}]", - "us_statuses": "[{\"name\": \"New\", \"is_archived\": false, \"wip_limit\": null, \"order\": 1, \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"name\": \"Ready\", \"is_archived\": false, \"wip_limit\": null, \"order\": 2, \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"name\": \"In progress\", \"is_archived\": false, \"wip_limit\": null, \"order\": 3, \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"name\": \"Ready for test\", \"is_archived\": false, \"wip_limit\": null, \"order\": 4, \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"name\": \"Done\", \"is_archived\": false, \"wip_limit\": null, \"order\": 5, \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}, {\"name\": \"Archived\", \"is_archived\": true, \"wip_limit\": null, \"order\": 6, \"color\": \"#5c3566\", \"slug\": \"archived\", \"is_closed\": true}]", - "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]", - "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#ffcc00\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#669900\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#999999\", \"slug\": \"needs-info\", \"is_closed\": false}]", - "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#8C2318\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#5E8C6A\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#88A65E\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#BFB35A\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#89BAB4\", \"slug\": \"needs-info\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"color\": \"#CC0000\", \"slug\": \"rejected\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"color\": \"#666666\", \"slug\": \"posponed\", \"is_closed\": false}]", - "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#89BAB4\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#ba89a8\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#89a8ba\"}]", - "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#666666\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#669933\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]", - "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#666666\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#669933\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#0000FF\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#FFA500\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]", - "roles": "[{\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false}]" + "default_options": "{\"issue_status\": \"New\", \"task_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"points\": \"?\", \"priority\": \"Normal\", \"us_status\": \"New\", \"epic_status\": \"New\"}", + "epic_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Ready\", \"is_closed\": false, \"slug\": \"ready\", \"order\": 2, \"color\": \"#ff8a84\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#ff9900\"}, {\"name\": \"Ready for test\", \"is_closed\": false, \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#fcc000\"}, {\"name\": \"Done\", \"is_closed\": true, \"slug\": \"done\", \"order\": 5, \"color\": \"#669900\"}]", + "us_statuses": "[{\"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"order\": 1}, {\"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#ff8a84\", \"order\": 2}, {\"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#ff9900\", \"order\": 3}, {\"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#fcc000\", \"order\": 4}, {\"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\", \"is_closed\": true, \"is_archived\": false, \"color\": \"#669900\", \"order\": 5}, {\"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\", \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"order\": 6}]", + "points": "[{\"name\": \"?\", \"value\": null, \"order\": 1}, {\"name\": \"0\", \"value\": 0.0, \"order\": 2}, {\"name\": \"1/2\", \"value\": 0.5, \"order\": 3}, {\"name\": \"1\", \"value\": 1.0, \"order\": 4}, {\"name\": \"2\", \"value\": 2.0, \"order\": 5}, {\"name\": \"3\", \"value\": 3.0, \"order\": 6}, {\"name\": \"5\", \"value\": 5.0, \"order\": 7}, {\"name\": \"8\", \"value\": 8.0, \"order\": 8}, {\"name\": \"10\", \"value\": 10.0, \"order\": 9}, {\"name\": \"13\", \"value\": 13.0, \"order\": 10}, {\"name\": \"20\", \"value\": 20.0, \"order\": 11}, {\"name\": \"40\", \"value\": 40.0, \"order\": 12}]", + "task_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#ff9900\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#ffcc00\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#669900\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#999999\"}]", + "issue_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#8C2318\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#5E8C6A\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#88A65E\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#BFB35A\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#89BAB4\"}, {\"name\": \"Rejected\", \"is_closed\": true, \"slug\": \"rejected\", \"order\": 6, \"color\": \"#CC0000\"}, {\"name\": \"Postponed\", \"is_closed\": false, \"slug\": \"posponed\", \"order\": 7, \"color\": \"#666666\"}]", + "issue_types": "[{\"name\": \"Bug\", \"order\": 1, \"color\": \"#89BAB4\"}, {\"name\": \"Question\", \"order\": 2, \"color\": \"#ba89a8\"}, {\"name\": \"Enhancement\", \"order\": 3, \"color\": \"#89a8ba\"}]", + "priorities": "[{\"name\": \"Low\", \"order\": 1, \"color\": \"#666666\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#669933\"}, {\"name\": \"High\", \"order\": 5, \"color\": \"#CC0000\"}]", + "severities": "[{\"name\": \"Wishlist\", \"order\": 1, \"color\": \"#666666\"}, {\"name\": \"Minor\", \"order\": 2, \"color\": \"#669933\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#0000FF\"}, {\"name\": \"Important\", \"order\": 4, \"color\": \"#FFA500\"}, {\"name\": \"Critical\", \"order\": 5, \"color\": \"#CC0000\"}]", + "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\"], \"order\": 60}]" } }, { @@ -38,7 +38,7 @@ "description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.", "order": 2, "created_date": "2014-04-22T14:50:19.738Z", - "modified_date": "2016-06-29T14:52:15.232Z", + "modified_date": "2016-07-07T13:18:28.186Z", "default_owner_role": "product-owner", "is_epics_activated": true, "is_backlog_activated": false, @@ -47,16 +47,16 @@ "is_issues_activated": false, "videoconferences": null, "videoconferences_extra_data": "", - "default_options": "{\"epic_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"us_status\": \"New\", \"points\": \"?\", \"priority\": \"Normal\", \"task_status\": \"New\", \"issue_status\": \"New\"}", - "epic_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"is_closed\": false}, {\"order\": 3, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 4, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"order\": 5, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"is_closed\": true}]", - "us_statuses": "[{\"name\": \"New\", \"is_archived\": false, \"wip_limit\": null, \"order\": 1, \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"name\": \"Ready\", \"is_archived\": false, \"wip_limit\": null, \"order\": 2, \"color\": \"#f57900\", \"slug\": \"ready\", \"is_closed\": false}, {\"name\": \"In progress\", \"is_archived\": false, \"wip_limit\": null, \"order\": 3, \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"name\": \"Ready for test\", \"is_archived\": false, \"wip_limit\": null, \"order\": 4, \"color\": \"#4e9a06\", \"slug\": \"ready-for-test\", \"is_closed\": false}, {\"name\": \"Done\", \"is_archived\": false, \"wip_limit\": null, \"order\": 5, \"color\": \"#cc0000\", \"slug\": \"done\", \"is_closed\": true}, {\"name\": \"Archived\", \"is_archived\": true, \"wip_limit\": null, \"order\": 6, \"color\": \"#5c3566\", \"slug\": \"archived\", \"is_closed\": true}]", - "points": "[{\"order\": 1, \"name\": \"?\", \"value\": null}, {\"order\": 2, \"name\": \"0\", \"value\": 0.0}, {\"order\": 3, \"name\": \"1/2\", \"value\": 0.5}, {\"order\": 4, \"name\": \"1\", \"value\": 1.0}, {\"order\": 5, \"name\": \"2\", \"value\": 2.0}, {\"order\": 6, \"name\": \"3\", \"value\": 3.0}, {\"order\": 7, \"name\": \"5\", \"value\": 5.0}, {\"order\": 8, \"name\": \"8\", \"value\": 8.0}, {\"order\": 9, \"name\": \"10\", \"value\": 10.0}, {\"order\": 10, \"name\": \"13\", \"value\": 13.0}, {\"order\": 11, \"name\": \"20\", \"value\": 20.0}, {\"order\": 12, \"name\": \"40\", \"value\": 40.0}]", - "task_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"is_closed\": false}]", - "issue_statuses": "[{\"order\": 1, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"is_closed\": false}, {\"order\": 2, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"is_closed\": false}, {\"order\": 3, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"is_closed\": true}, {\"order\": 4, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"is_closed\": true}, {\"order\": 5, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"is_closed\": false}, {\"order\": 6, \"name\": \"Rejected\", \"color\": \"#d3d7cf\", \"slug\": \"rejected\", \"is_closed\": true}, {\"order\": 7, \"name\": \"Postponed\", \"color\": \"#75507b\", \"slug\": \"posponed\", \"is_closed\": false}]", - "issue_types": "[{\"order\": 1, \"name\": \"Bug\", \"color\": \"#cc0000\"}, {\"order\": 2, \"name\": \"Question\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Enhancement\", \"color\": \"#4e9a06\"}]", - "priorities": "[{\"order\": 1, \"name\": \"Low\", \"color\": \"#999999\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 5, \"name\": \"High\", \"color\": \"#CC0000\"}]", - "severities": "[{\"order\": 1, \"name\": \"Wishlist\", \"color\": \"#999999\"}, {\"order\": 2, \"name\": \"Minor\", \"color\": \"#729fcf\"}, {\"order\": 3, \"name\": \"Normal\", \"color\": \"#4e9a06\"}, {\"order\": 4, \"name\": \"Important\", \"color\": \"#f57900\"}, {\"order\": 5, \"name\": \"Critical\", \"color\": \"#CC0000\"}]", - "roles": "[{\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 10, \"name\": \"UX\", \"slug\": \"ux\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 20, \"name\": \"Design\", \"slug\": \"design\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 30, \"name\": \"Front\", \"slug\": \"front\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 40, \"name\": \"Back\", \"slug\": \"back\", \"computable\": true}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 50, \"name\": \"Product Owner\", \"slug\": \"product-owner\", \"computable\": false}, {\"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\"], \"order\": 60, \"name\": \"Stakeholder\", \"slug\": \"stakeholder\", \"computable\": false}]" + "default_options": "{\"issue_status\": \"New\", \"task_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"points\": \"?\", \"priority\": \"Normal\", \"us_status\": \"New\", \"epic_status\": \"New\"}", + "epic_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Ready\", \"is_closed\": false, \"slug\": \"ready\", \"order\": 2, \"color\": \"#ff8a84\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#ff9900\"}, {\"name\": \"Ready for test\", \"is_closed\": false, \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#fcc000\"}, {\"name\": \"Done\", \"is_closed\": true, \"slug\": \"done\", \"order\": 5, \"color\": \"#669900\"}]", + "us_statuses": "[{\"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"order\": 1}, {\"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#f57900\", \"order\": 2}, {\"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#729fcf\", \"order\": 3}, {\"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#4e9a06\", \"order\": 4}, {\"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\", \"is_closed\": true, \"is_archived\": false, \"color\": \"#cc0000\", \"order\": 5}, {\"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\", \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"order\": 6}]", + "points": "[{\"name\": \"?\", \"value\": null, \"order\": 1}, {\"name\": \"0\", \"value\": 0.0, \"order\": 2}, {\"name\": \"1/2\", \"value\": 0.5, \"order\": 3}, {\"name\": \"1\", \"value\": 1.0, \"order\": 4}, {\"name\": \"2\", \"value\": 2.0, \"order\": 5}, {\"name\": \"3\", \"value\": 3.0, \"order\": 6}, {\"name\": \"5\", \"value\": 5.0, \"order\": 7}, {\"name\": \"8\", \"value\": 8.0, \"order\": 8}, {\"name\": \"10\", \"value\": 10.0, \"order\": 9}, {\"name\": \"13\", \"value\": 13.0, \"order\": 10}, {\"name\": \"20\", \"value\": 20.0, \"order\": 11}, {\"name\": \"40\", \"value\": 40.0, \"order\": 12}]", + "task_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#f57900\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#4e9a06\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#cc0000\"}]", + "issue_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#f57900\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#4e9a06\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#cc0000\"}, {\"name\": \"Rejected\", \"is_closed\": true, \"slug\": \"rejected\", \"order\": 6, \"color\": \"#d3d7cf\"}, {\"name\": \"Postponed\", \"is_closed\": false, \"slug\": \"posponed\", \"order\": 7, \"color\": \"#75507b\"}]", + "issue_types": "[{\"name\": \"Bug\", \"order\": 1, \"color\": \"#cc0000\"}, {\"name\": \"Question\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Enhancement\", \"order\": 3, \"color\": \"#4e9a06\"}]", + "priorities": "[{\"name\": \"Low\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#4e9a06\"}, {\"name\": \"High\", \"order\": 5, \"color\": \"#CC0000\"}]", + "severities": "[{\"name\": \"Wishlist\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Minor\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#4e9a06\"}, {\"name\": \"Important\", \"order\": 4, \"color\": \"#f57900\"}, {\"name\": \"Critical\", \"order\": 5, \"color\": \"#CC0000\"}]", + "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\"], \"order\": 60}]" } } ] diff --git a/taiga/projects/migrations/0049_auto_20160629_1443.py b/taiga/projects/migrations/0049_auto_20160629_1443.py index cdfd420b..417875e5 100644 --- a/taiga/projects/migrations/0049_auto_20160629_1443.py +++ b/taiga/projects/migrations/0049_auto_20160629_1443.py @@ -81,12 +81,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='project', name='anon_permissions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_epic', 'View epic'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('view_epics', 'View epic'), ('view_us', 'View user stories'), ('view_tasks', 'View tasks'), ('view_issues', 'View issues'), ('view_wiki_pages', 'View wiki pages'), ('view_wiki_links', 'View wiki links')]), blank=True, default=[], null=True, size=None, verbose_name='anonymous permissions'), ), migrations.AlterField( model_name='project', name='public_permissions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epic', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epics', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='user permissions'), ), migrations.AddField( model_name='epicstatus', diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 06964eb3..94d37c80 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -160,7 +160,7 @@ def filter_timeline_for_user(timeline, user): content_types = { "view_project": ContentType.objects.get_by_natural_key("projects", "project"), "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"), - "view_epic": ContentType.objects.get_by_natural_key("epics", "epic"), + "view_epics": ContentType.objects.get_by_natural_key("epics", "epic"), "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"), "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"), "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"), diff --git a/taiga/users/migrations/0022_auto_20160629_1443.py b/taiga/users/migrations/0022_auto_20160629_1443.py index 2acaf944..68a65443 100644 --- a/taiga/users/migrations/0022_auto_20160629_1443.py +++ b/taiga/users/migrations/0022_auto_20160629_1443.py @@ -16,6 +16,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='role', name='permissions', - field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epic', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'), + field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(choices=[('view_project', 'View project'), ('view_milestones', 'View milestones'), ('add_milestone', 'Add milestone'), ('modify_milestone', 'Modify milestone'), ('delete_milestone', 'Delete milestone'), ('view_epics', 'View epic'), ('add_epic', 'Add epic'), ('modify_epic', 'Modify epic'), ('comment_epic', 'Comment epic'), ('delete_epic', 'Delete epic'), ('view_us', 'View user story'), ('add_us', 'Add user story'), ('modify_us', 'Modify user story'), ('comment_us', 'Comment user story'), ('delete_us', 'Delete user story'), ('view_tasks', 'View tasks'), ('add_task', 'Add task'), ('modify_task', 'Modify task'), ('comment_task', 'Comment task'), ('delete_task', 'Delete task'), ('view_issues', 'View issues'), ('add_issue', 'Add issue'), ('modify_issue', 'Modify issue'), ('comment_issue', 'Comment issue'), ('delete_issue', 'Delete issue'), ('view_wiki_pages', 'View wiki pages'), ('add_wiki_page', 'Add wiki page'), ('modify_wiki_page', 'Modify wiki page'), ('comment_wiki_page', 'Comment wiki page'), ('delete_wiki_page', 'Delete wiki page'), ('view_wiki_links', 'View wiki links'), ('add_wiki_link', 'Add wiki link'), ('modify_wiki_link', 'Modify wiki link'), ('delete_wiki_link', 'Delete wiki link')]), blank=True, default=[], null=True, size=None, verbose_name='permissions'), ), ] diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py new file mode 100644 index 00000000..ceffe206 --- /dev/null +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -0,0 +1,901 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import uuid + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.epics.serializers import EpicSerializer +from taiga.projects.epics.models import Epic +from taiga.projects.epics.utils import attach_extra_info as attach_epic_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.occ import OCCResourceMixin + +from tests import factories as f +from tests.utils import helper_test_http_method, reconnect_signals +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher + +from unittest import mock + +import pytest +pytestmark = pytest.mark.django_db + + +def setup_function(function): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner) + #epics_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner) + #epics_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner) + #epics_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + #epics_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + m.public_epic = f.EpicFactory(project=m.public_project, + status__project=m.public_project) + m.public_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.public_epic.id) + + m.private_epic1 = f.EpicFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_epic1 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic1.id) + + m.private_epic2 = f.EpicFactory(project=m.private_project2, + status__project=m.private_project2) + m.private_epic2 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic2.id) + + m.blocked_epic = f.EpicFactory(project=m.blocked_project, + status__project=m.blocked_project) + m.blocked_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.blocked_epic.id) + + m.public_project.default_epic_status = m.public_epic.status + m.public_project.save() + m.private_project1.default_epic_status = m.private_epic1.status + m.private_project1.save() + m.private_project2.default_epic_status = m.private_epic2.status + m.private_project2.save() + m.blocked_project.default_epic_status = m.blocked_epic.status + m.blocked_project.save() + + return m + + +def test_epic_list(client, data): + url = reverse('epics-list') + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 2 + assert response.status_code == 200 + + client.login(data.registered_user) + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 2 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 4 + assert response.status_code == 200 + + client.login(data.project_owner) + + response = client.get(url) + epics_data = json.loads(response.content.decode('utf-8')) + assert len(epics_data) == 4 + assert response.status_code == 200 + + +def test_epic_retrieve(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_create(client, data): + url = reverse('epics-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "subject": "test", + "ref": 1, + "project": data.public_project.pk, + "status": data.public_project.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 2, + "project": data.private_project1.pk, + "status": data.private_project1.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.private_project2.pk, + "status": data.private_project2.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "subject": "test", + "ref": 3, + "project": data.blocked_project.pk, + "status": data.blocked_project.epic_statuses.all()[0].pk, + }) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_update(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + epic_data = EpicSerializer(data.public_epic).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic1).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic2).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.blocked_epic).data + epic_data["subject"] = "test" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + epic_data = EpicSerializer(data.public_epic).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic1).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic2).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.blocked_epic).data + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_update_and_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + epic_data = EpicSerializer(data.public_epic).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', public_url, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic1).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url1, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.private_epic2).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', private_url2, epic_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_data = EpicSerializer(data.blocked_epic).data + epic_data["subject"] = "test" + epic_data["comment"] = "test comment" + epic_data = json.dumps(epic_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_put_update_with_project_change(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + user3 = f.UserFactory.create() + user4 = f.UserFactory.create() + project1 = f.ProjectFactory() + project2 = f.ProjectFactory() + + epic_status1 = f.EpicStatusFactory.create(project=project1) + epic_status2 = f.EpicStatusFactory.create(project=project2) + + project1.default_epic_status = epic_status1 + project2.default_epic_status = epic_status2 + + project1.save() + project2.save() + + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + epic = f.EpicFactory.create(project=project1) + epic = attach_epic_extra_info(Epic.objects.all()).get(id=epic.id) + + url = reverse('epics-detail', kwargs={"pk": epic.pk}) + + # Test user with permissions in both projects + client.login(user1) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 200 + + epic.project = project1 + epic.save() + + # Test user with permissions in only origin project + client.login(user2) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 403 + + epic.project = project1 + epic.save() + + # Test user with permissions in only destionation project + client.login(user3) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 403 + + epic.project = project1 + epic.save() + + # Test user without permissions in the projects + client.login(user4) + + epic_data = EpicSerializer(epic).data + epic_data["project"] = project2.id + epic_data = json.dumps(epic_data) + + response = client.put(url, data=epic_data, content_type="application/json") + + assert response.status_code == 403 + + epic.project = project1 + epic.save() + + +def test_epic_patch_update(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"subject": "test", "version": data.public_epic.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_epic1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.private_epic2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"subject": "test", "version": data.blocked_epic.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_patch_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({"comment": "test comment", "version": data.public_epic.version}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_epic1.version}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.private_epic2.version}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({"comment": "test comment", "version": data.blocked_epic.version}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_patch_update_and_comment(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.public_epic.version + }) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_epic1.version + }) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.private_epic2.version + }) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + patch_data = json.dumps({ + "subject": "test", + "comment": "test comment", + "version": data.blocked_epic.version + }) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_delete(client, data): + public_url = reverse('epics-detail', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-detail', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-detail', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-detail', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + +def test_epic_action_bulk_create(client, data): + url = reverse('epics-bulk-create') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.public_epic.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.private_epic1.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.private_epic2.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_epics": "test1\ntest2", + "project_id": data.blocked_epic.project.pk, + }) + results = helper_test_http_method(client, 'post', url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_action_upvote(client, data): + public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-upvote', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-upvote', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_action_downvote(client, data): + public_url = reverse('epics-downvote', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-downvote', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-downvote', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-downvote', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_voters_list(client, data): + public_url = reverse('epic-voters-list', kwargs={"resource_id": data.public_epic.pk}) + private_url1 = reverse('epic-voters-list', kwargs={"resource_id": data.private_epic1.pk}) + private_url2 = reverse('epic-voters-list', kwargs={"resource_id": data.private_epic2.pk}) + blocked_url = reverse('epic-voters-list', kwargs={"resource_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_voters_retrieve(client, data): + add_vote(data.public_epic, data.project_owner) + public_url = reverse('epic-voters-detail', kwargs={"resource_id": data.public_epic.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_epic1, data.project_owner) + private_url1 = reverse('epic-voters-detail', kwargs={"resource_id": data.private_epic1.pk, + "pk": data.project_owner.pk}) + add_vote(data.private_epic2, data.project_owner) + private_url2 = reverse('epic-voters-detail', kwargs={"resource_id": data.private_epic2.pk, + "pk": data.project_owner.pk}) + + add_vote(data.blocked_epic, data.project_owner) + blocked_url = reverse('epic-voters-detail', kwargs={"resource_id": data.blocked_epic.pk, + "pk": data.project_owner.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_action_watch(client, data): + public_url = reverse('epics-watch', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-watch', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-watch', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-watch', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_action_unwatch(client, data): + public_url = reverse('epics-unwatch', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-unwatch', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-unwatch', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-unwatch', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'post', public_url, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, "", users) + assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, "", users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, "", users) + assert results == [404, 404, 404, 451, 451] + + +def test_epic_watchers_list(client, data): + public_url = reverse('epic-watchers-list', kwargs={"resource_id": data.public_epic.pk}) + private_url1 = reverse('epic-watchers-list', kwargs={"resource_id": data.private_epic1.pk}) + private_url2 = reverse('epic-watchers-list', kwargs={"resource_id": data.private_epic2.pk}) + blocked_url = reverse('epic-watchers-list', kwargs={"resource_id": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_watchers_retrieve(client, data): + add_watcher(data.public_epic, data.project_owner) + public_url = reverse('epic-watchers-detail', kwargs={"resource_id": data.public_epic.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_epic1, data.project_owner) + private_url1 = reverse('epic-watchers-detail', kwargs={"resource_id": data.private_epic1.pk, + "pk": data.project_owner.pk}) + add_watcher(data.private_epic2, data.project_owner) + private_url2 = reverse('epic-watchers-detail', kwargs={"resource_id": data.private_epic2.pk, + "pk": data.project_owner.pk}) + + add_watcher(data.blocked_epic, data.project_owner) + blocked_url = reverse('epic-watchers-detail', kwargs={"resource_id": data.blocked_epic.pk, + "pk": data.project_owner.pk}) + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +#def test_epics_csv(client, data): +# url = reverse('epics-csv') +# csv_public_uuid = data.public_project.epics_csv_uuid +# csv_private1_uuid = data.private_project1.epics_csv_uuid +# csv_private2_uuid = data.private_project1.epics_csv_uuid +# csv_blocked_uuid = data.blocked_project.epics_csv_uuid +# +# users = [ +# None, +# data.registered_user, +# data.project_member_without_perms, +# data.project_member_with_perms, +# data.project_owner +# ] +# +# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) +# assert results == [200, 200, 200, 200, 200] +# +# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) +# assert results == [200, 200, 200, 200, 200] +# +# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) +# assert results == [200, 200, 200, 200, 200] +# +# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) +# assert results == [200, 200, 200, 200, 200] From 1ae4e49246f15494bed9f70e8ace4e57f951296d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 19 Jul 2016 13:54:30 +0200 Subject: [PATCH 167/261] Add Epics to taiga.searches --- taiga/searches/api.py | 9 +++++++ taiga/searches/serializers.py | 26 ++++++++++++------- taiga/searches/services.py | 15 ++++++++--- tests/integration/test_searches.py | 41 +++++++++++++++++++++++++++--- 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/taiga/searches/api.py b/taiga/searches/api.py index 37f9c7f3..0c252b5a 100644 --- a/taiga/searches/api.py +++ b/taiga/searches/api.py @@ -40,6 +40,10 @@ class SearchViewSet(viewsets.ViewSet): result = {} with futures.ThreadPoolExecutor(max_workers=4) as executor: futures_list = [] + if user_has_perm(request.user, "view_epics", project): + epics_future = executor.submit(self._search_epics, project, text) + epics_future.result_key = "epics" + futures_list.append(epics_future) if user_has_perm(request.user, "view_us", project): uss_future = executor.submit(self._search_user_stories, project, text) uss_future.result_key = "userstories" @@ -73,6 +77,11 @@ class SearchViewSet(viewsets.ViewSet): project_model = apps.get_model("projects", "Project") return get_object_or_404(project_model, pk=project_id) + def _search_epics(self, project, text): + queryset = services.search_epics(project, text) + serializer = serializers.EpicSearchResultsSerializer(queryset, many=True) + return serializer.data + def _search_user_stories(self, project, text): queryset = services.search_user_stories(project, text) serializer = serializers.UserStorySearchResultsSerializer(queryset, many=True) diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py index e96e1131..7adc34b2 100644 --- a/taiga/searches/serializers.py +++ b/taiga/searches/serializers.py @@ -20,15 +20,7 @@ from taiga.base.api import serializers from taiga.base.fields import Field, MethodField -class IssueSearchResultsSerializer(serializers.LightSerializer): - id = Field() - ref = Field() - subject = Field() - status = Field(attr="status_id") - assigned_to = Field(attr="assigned_to_id") - - -class TaskSearchResultsSerializer(serializers.LightSerializer): +class EpicSearchResultsSerializer(serializers.LightSerializer): id = Field() ref = Field() subject = Field() @@ -58,6 +50,22 @@ class UserStorySearchResultsSerializer(serializers.LightSerializer): return obj.total_points_attr +class TaskSearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + assigned_to = Field(attr="assigned_to_id") + + +class IssueSearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + assigned_to = Field(attr="assigned_to_id") + + class WikiPageSearchResultsSerializer(serializers.LightSerializer): id = Field() slug = Field() diff --git a/taiga/searches/services.py b/taiga/searches/services.py index afc6a7e5..adda60bb 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -24,6 +24,13 @@ from taiga.projects.userstories.utils import attach_total_points MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) +def search_epics(project, text): + model = apps.get_model("epics", "Epic") + queryset = model.objects.filter(project_id=project.pk) + table = "epics_epic" + return _search_items(queryset, table, text) + + def search_user_stories(project, text): model = apps.get_model("userstories", "UserStory") queryset = model.objects.filter(project_id=project.pk) @@ -32,16 +39,16 @@ def search_user_stories(project, text): def search_tasks(project, text): - model = apps.get_model("userstories", "UserStory") + model = apps.get_model("tasks", "Task") queryset = model.objects.filter(project_id=project.pk) - table = "userstories_userstory" + table = "tasks_task" return _search_items(queryset, table, text) def search_issues(project, text): - model = apps.get_model("userstories", "UserStory") + model = apps.get_model("issues", "Issue") queryset = model.objects.filter(project_id=project.pk) - table = "userstories_userstory" + table = "issues_issue" return _search_items(queryset, table, text) diff --git a/tests/integration/test_searches.py b/tests/integration/test_searches.py index 1ccd5233..bb5e681d 100644 --- a/tests/integration/test_searches.py +++ b/tests/integration/test_searches.py @@ -53,6 +53,12 @@ def searches_initial_data(): role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.epic11 = f.EpicFactory(project=m.project1, subject="Back to the future") + m.epic12 = f.EpicFactory(project=m.project1, tags=["Back", "future"]) + m.epic13 = f.EpicFactory(project=m.project1) + m.epic14 = f.EpicFactory(project=m.project1, description="Backend to the future") + m.epic21 = f.EpicFactory(project=m.project2, subject="Back to the future") + m.us11 = f.UserStoryFactory(project=m.project1, subject="Back to the future") m.us12 = f.UserStoryFactory(project=m.project1, description="Back to the future") m.us13 = f.UserStoryFactory(project=m.project1, tags=["Backend", "future"]) @@ -87,7 +93,8 @@ def test_search_all_objects_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id}) assert response.status_code == 200 - assert response.data["count"] == 16 + assert response.data["count"] == 20 + assert len(response.data["epics"]) == 4 assert len(response.data["userstories"]) == 4 assert len(response.data["tasks"]) == 4 assert len(response.data["issues"]) == 4 @@ -111,20 +118,48 @@ def test_search_text_query_in_my_project(client, searches_initial_data): response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "future"}) assert response.status_code == 200 - assert response.data["count"] == 9 + assert response.data["count"] == 12 + assert len(response.data["epics"]) == 3 + assert response.data["epics"][0]["id"] == searches_initial_data.epic11.id + assert response.data["epics"][1]["id"] == searches_initial_data.epic12.id + assert response.data["epics"][2]["id"] == searches_initial_data.epic14.id assert len(response.data["userstories"]) == 3 + assert response.data["userstories"][0]["id"] == searches_initial_data.us11.id + assert response.data["userstories"][1]["id"] == searches_initial_data.us13.id + assert response.data["userstories"][2]["id"] == searches_initial_data.us12.id assert len(response.data["tasks"]) == 3 + assert response.data["tasks"][0]["id"] == searches_initial_data.task11.id + assert response.data["tasks"][1]["id"] == searches_initial_data.task12.id + assert response.data["tasks"][2]["id"] == searches_initial_data.task14.id assert len(response.data["issues"]) == 3 + assert response.data["issues"][0]["id"] == searches_initial_data.issue14.id + assert response.data["issues"][1]["id"] == searches_initial_data.issue12.id + assert response.data["issues"][2]["id"] == searches_initial_data.issue11.id assert len(response.data["wikipages"]) == 0 response = client.get(reverse("search-list"), {"project": data.project1.id, "text": "back"}) assert response.status_code == 200 - assert response.data["count"] == 11 + assert response.data["count"] == 14 + assert len(response.data["epics"]) == 3 + assert response.data["epics"][0]["id"] == searches_initial_data.epic11.id + assert response.data["epics"][1]["id"] == searches_initial_data.epic12.id + assert response.data["epics"][2]["id"] == searches_initial_data.epic14.id assert len(response.data["userstories"]) == 3 + assert response.data["userstories"][0]["id"] == searches_initial_data.us11.id + assert response.data["userstories"][1]["id"] == searches_initial_data.us13.id + assert response.data["userstories"][2]["id"] == searches_initial_data.us12.id assert len(response.data["tasks"]) == 3 + assert response.data["tasks"][0]["id"] == searches_initial_data.task11.id + assert response.data["tasks"][1]["id"] == searches_initial_data.task12.id + assert response.data["tasks"][2]["id"] == searches_initial_data.task14.id assert len(response.data["issues"]) == 3 + assert response.data["issues"][0]["id"] == searches_initial_data.issue14.id + assert response.data["issues"][1]["id"] == searches_initial_data.issue12.id + assert response.data["issues"][2]["id"] == searches_initial_data.issue11.id # Back is a backend substring assert len(response.data["wikipages"]) == 2 + assert response.data["wikipages"][0]["id"] == searches_initial_data.wikipage14.id + assert response.data["wikipages"][1]["id"] == searches_initial_data.wikipage13.id def test_search_text_query_with_an_invalid_project_id(client, searches_initial_data): From a752c6b84fb01b74b9235b476c5120cc7dc75116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 19 Jul 2016 20:12:05 +0200 Subject: [PATCH 168/261] Add tests over Epic attachments --- tests/factories.py | 11 + .../test_attachment_resources.py | 221 +++++++++++++++++- 2 files changed, 228 insertions(+), 4 deletions(-) diff --git a/tests/factories.py b/tests/factories.py index 8558907d..2e831c13 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -121,6 +121,17 @@ class RolePointsFactory(Factory): points = factory.SubFactory("tests.factories.PointsFactory") +class EpicAttachmentFactory(Factory): + project = factory.SubFactory("tests.factories.ProjectFactory") + owner = factory.SubFactory("tests.factories.UserFactory") + content_object = factory.SubFactory("tests.factories.EpicFactory") + attached_file = factory.django.FileField(data=b"File contents") + + class Meta: + model = "attachments.Attachment" + strategy = factory.CREATE_STRATEGY + + class UserStoryAttachmentFactory(Factory): project = factory.SubFactory("tests.factories.ProjectFactory") owner = factory.SubFactory("tests.factories.UserFactory") diff --git a/tests/integration/resources_permissions/test_attachment_resources.py b/tests/integration/resources_permissions/test_attachment_resources.py index c30fb854..456c96f2 100644 --- a/tests/integration/resources_permissions/test_attachment_resources.py +++ b/tests/integration/resources_permissions/test_attachment_resources.py @@ -120,6 +120,20 @@ def data(): return m +@pytest.fixture +def data_epic(data): + m = type("Models", (object,), {}) + m.public_epic = f.EpicFactory(project=data.public_project, ref=20) + m.public_epic_attachment = f.EpicAttachmentFactory(project=data.public_project, content_object=m.public_epic) + m.private_epic1 = f.EpicFactory(project=data.private_project1, ref=21) + m.private_epic1_attachment = f.EpicAttachmentFactory(project=data.private_project1, content_object=m.private_epic1) + m.private_epic2 = f.EpicFactory(project=data.private_project2, ref=22) + m.private_epic2_attachment = f.EpicAttachmentFactory(project=data.private_project2, content_object=m.private_epic2) + m.blocked_epic = f.EpicFactory(project=data.blocked_project, ref=23) + m.blocked_epic_attachment = f.EpicAttachmentFactory(project=data.blocked_project, content_object=m.blocked_epic) + return m + + @pytest.fixture def data_us(data): m = type("Models", (object,), {}) @@ -180,6 +194,30 @@ def data_wiki(data): return m +def test_epic_attachment_retrieve(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + def test_user_story_attachment_retrieve(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) @@ -276,6 +314,41 @@ def test_wiki_attachment_retrieve(client, data, data_wiki): assert results == [401, 403, 403, 200, 200] +def test_epic_attachment_update(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = AttachmentSerializer(data_epic.public_epic_attachment).data + attachment_data["description"] = "test" + attachment_data = json.dumps(attachment_data) + + results = helper_test_http_method(client, 'put', public_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) + assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 200, 200] + + def test_user_story_attachment_update(client, data, data_us): public_url = reverse("userstory-attachments-detail", args=[data_us.public_user_story_attachment.pk]) @@ -299,20 +372,20 @@ def test_user_story_attachment_update(client, data, data_us): attachment_data = json.dumps(attachment_data) results = helper_test_http_method(client, "put", public_url, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] results = helper_test_http_method(client, "put", private_url1, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] results = helper_test_http_method(client, "put", private_url2, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] results = helper_test_http_method(client, "put", blocked_url, attachment_data, users) - # assert results == [401, 403, 403, 400, 400] assert results == [405, 405, 405, 405, 405] + # assert results == [401, 403, 403, 400, 400] def test_task_attachment_update(client, data, data_task): @@ -336,12 +409,15 @@ def test_task_attachment_update(client, data, data_task): results = helper_test_http_method(client, 'put', public_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] @@ -368,12 +444,15 @@ def test_issue_attachment_update(client, data, data_issue): results = helper_test_http_method(client, 'put', public_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] @@ -400,17 +479,50 @@ def test_wiki_attachment_update(client, data, data_wiki): results = helper_test_http_method(client, 'put', public_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'put', private_url1, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 200, 200, 200, 200] + results = helper_test_http_method(client, 'put', private_url2, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'put', blocked_url, attachment_data, users) assert results == [405, 405, 405, 405, 405] # assert results == [401, 403, 403, 200, 200] +def test_epic_attachment_patch(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test"} + attachment_data = json.dumps(attachment_data) + + results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) + assert results == [401, 403, 403, 451, 451] + + def test_user_story_attachment_patch(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) @@ -430,10 +542,13 @@ def test_user_story_attachment_patch(client, data, data_us): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] @@ -457,10 +572,13 @@ def test_task_attachment_patch(client, data, data_task): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] @@ -484,10 +602,13 @@ def test_issue_attachment_patch(client, data, data_issue): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] @@ -511,14 +632,43 @@ def test_wiki_attachment_patch(client, data, data_wiki): results = helper_test_http_method(client, 'patch', public_url, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url1, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', private_url2, attachment_data, users) assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'patch', blocked_url, attachment_data, users) assert results == [401, 403, 403, 451, 451] +def test_epic_attachment_delete(client, data, data_epic): + public_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.public_epic_attachment.pk}) + private_url1 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic1_attachment.pk}) + private_url2 = reverse('epic-attachments-detail', kwargs={"pk": data_epic.private_epic2_attachment.pk}) + blocked_url = reverse('epic-attachments-detail', kwargs={"pk": data_epic.blocked_epic_attachment.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + def test_user_story_attachment_delete(client, data, data_us): public_url = reverse('userstory-attachments-detail', kwargs={"pk": data_us.public_user_story_attachment.pk}) private_url1 = reverse('userstory-attachments-detail', kwargs={"pk": data_us.private_user_story1_attachment.pk}) @@ -534,10 +684,13 @@ def test_user_story_attachment_delete(client, data, data_us): results = helper_test_http_method(client, 'delete', public_url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] @@ -557,10 +710,13 @@ def test_task_attachment_delete(client, data, data_task): results = helper_test_http_method(client, 'delete', public_url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] @@ -580,10 +736,13 @@ def test_issue_attachment_delete(client, data, data_issue): results = helper_test_http_method(client, 'delete', public_url, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] @@ -603,14 +762,53 @@ def test_wiki_attachment_delete(client, data, data_wiki): results = helper_test_http_method(client, 'delete', public_url, None, [None, data.registered_user]) assert results == [401, 403] + results = helper_test_http_method(client, 'delete', private_url1, None, [None, data.registered_user]) assert results == [401, 403] + results = helper_test_http_method(client, 'delete', private_url2, None, users) assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) assert results == [401, 403, 403, 451] +def test_epic_attachment_create(client, data, data_epic): + url = reverse('epic-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + attachment_data = {"description": "test", + "object_id": data_epic.public_epic_attachment.object_id, + "project": data_epic.public_epic_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + assert results == [401, 403, 403, 201, 201] + + attachment_data = {"description": "test", + "object_id": data_epic.blocked_epic_attachment.object_id, + "project": data_epic.blocked_epic_attachment.project_id, + "attached_file": SimpleUploadedFile("test.txt", b"test")} + + _after_each_request_hook = lambda: attachment_data["attached_file"].seek(0) + + results = helper_test_http_method(client, 'post', url, attachment_data, users, + content_type=MULTIPART_CONTENT, + after_each_request=_after_each_request_hook) + assert results == [401, 403, 403, 451, 451] + + def test_user_story_attachment_create(client, data, data_us): url = reverse('userstory-attachments-list') @@ -756,6 +954,21 @@ def test_wiki_attachment_create(client, data, data_wiki): assert results == [401, 403, 403, 451, 451] +def test_epic_attachment_list(client, data, data_epic): + url = reverse('epic-attachments-list') + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method_and_count(client, 'get', url, None, users) + assert results == [(200, 2), (200, 2), (200, 2), (200, 4), (200, 4)] + + def test_user_story_attachment_list(client, data, data_us): url = reverse('userstory-attachments-list') From 0e2ba632db46425e4388767a7800a3545cdd43cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 19 Jul 2016 21:03:31 +0200 Subject: [PATCH 169/261] Add and fix some tests --- .../test_modules_resources.py | 24 +++++++++---------- .../test_resolver_resources.py | 14 +++++++++-- .../test_search_resources.py | 2 +- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/integration/resources_permissions/test_modules_resources.py b/tests/integration/resources_permissions/test_modules_resources.py index 34ad7369..68f552f4 100644 --- a/tests/integration/resources_permissions/test_modules_resources.py +++ b/tests/integration/resources_permissions/test_modules_resources.py @@ -211,18 +211,18 @@ def test_modules_patch(client, data): ] with mock.patch.object(OCCResourceMixin, "_validate_and_update_version"): - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', public_url, patch_data, users) - assert results == [401, 403, 403, 403, 204] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 403, 204] - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) - assert results == [401, 403, 403, 403, 204] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 403, 204] - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) - assert results == [404, 404, 404, 403, 204] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [404, 404, 404, 403, 204] - patch_data = json.dumps({"att": "test"}) - results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) - assert results == [404, 404, 404, 403, 451] + patch_data = json.dumps({"att": "test"}) + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [404, 404, 404, 403, 451] diff --git a/tests/integration/resources_permissions/test_resolver_resources.py b/tests/integration/resources_permissions/test_resolver_resources.py index b8c43d04..7e98b692 100644 --- a/tests/integration/resources_permissions/test_resolver_resources.py +++ b/tests/integration/resources_permissions/test_resolver_resources.py @@ -100,6 +100,7 @@ def data(): role__project=m.private_project2, role__permissions=["view_project"]) + m.epic = f.EpicFactory(project=m.private_project2, ref=4) m.us = f.UserStoryFactory(project=m.private_project2, ref=1) m.task = f.TaskFactory(project=m.private_project2, ref=2) m.issue = f.IssueFactory(project=m.private_project2, ref=3) @@ -127,8 +128,9 @@ def test_resolver_list(client, data): assert results == [401, 403, 403, 200, 200] client.login(data.other_user) - response = client.json.get("{}?project={}&us={}&task={}&issue={}&milestone={}".format(url, + response = client.json.get("{}?project={}&epic={}&us={}&task={}&issue={}&milestone={}".format(url, data.private_project2.slug, + data.epic.ref, data.us.ref, data.task.ref, data.issue.ref, @@ -136,18 +138,26 @@ def test_resolver_list(client, data): assert response.data == {"project": data.private_project2.pk} client.login(data.project_owner) - response = client.json.get("{}?project={}&us={}&task={}&issue={}&milestone={}".format(url, + response = client.json.get("{}?project={}&epic={}&us={}&task={}&issue={}&milestone={}".format(url, data.private_project2.slug, + data.epic.ref, data.us.ref, data.task.ref, data.issue.ref, data.milestone.slug)) assert response.data == {"project": data.private_project2.pk, + "epic": data.epic.pk, "us": data.us.pk, "task": data.task.pk, "issue": data.issue.pk, "milestone": data.milestone.pk} + response = client.json.get("{}?project={}&ref={}".format(url, + data.private_project2.slug, + data.epic.ref)) + assert response.data == {"project": data.private_project2.pk, + "epic": data.epic.pk} + response = client.json.get("{}?project={}&ref={}".format(url, data.private_project2.slug, data.us.ref)) diff --git a/tests/integration/resources_permissions/test_search_resources.py b/tests/integration/resources_permissions/test_search_resources.py index 7ce732be..633b3131 100644 --- a/tests/integration/resources_permissions/test_search_resources.py +++ b/tests/integration/resources_permissions/test_search_resources.py @@ -126,7 +126,7 @@ def test_search_list(client, data): ] results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.public_project.pk}, users) - all_keys = set(['count', 'userstories', 'issues', 'tasks', 'wikipages']) + all_keys = set(['count', 'userstories', 'issues', 'tasks', 'wikipages', 'epics']) assert results == [(200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys)] results = helper_test_http_method_and_keys(client, 'get', url, {'project': data.private_project1.pk}, users) assert results == [(200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys), (200, all_keys)] From f2bfa82c4475527c6479e90c69973ac380371340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 10:02:50 +0200 Subject: [PATCH 170/261] Fix sampledata --- taiga/projects/management/commands/sample_data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 70ec16a1..ea48c4e9 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -557,7 +557,11 @@ class Command(BaseCommand): filters = {"project": epic.project} n = self.sd.choice(list(range(self.sd.int(*NUM_USS_EPICS)))) - epic.user_stories.add(*UserStory.objects.filter(**filters).order_by("?")[:n]) + user_stories = UserStory.objects.filter(**filters).order_by("?")[:n] + for idx, us in enumerate(list(user_stories)): + RelatedUserStory.objects.create(epic=epic, + user_story=us, + order=idx+1) return epic From 5d5a42a077e816ba7b16ffae92f7c5cfed9c770c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 11:01:08 +0200 Subject: [PATCH 171/261] Create voters and watcher for epics --- taiga/projects/management/commands/sample_data.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index ea48c4e9..12e3f4e1 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -546,10 +546,8 @@ class Command(BaseCommand): # comment=self.sd.paragraph(), # user=epic.owner) - # TODO: Epic voters - #self.create_votes(epic) - # TODO: Epic watchers - #self.create_watchers(epic) + self.create_votes(epic) + self.create_watchers(epic) if self.sd.choice([True, True, False, True, True]): filters = {} From 0d3de76c8ed6d1d14a7ba8093ec2dc006adc4c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 11:13:13 +0200 Subject: [PATCH 172/261] Fix resolve endpoint to work with epics --- taiga/projects/epics/models.py | 1 - taiga/projects/references/api.py | 8 ++++++++ taiga/projects/references/validators.py | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index d55c1dff..75380556 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -88,7 +88,6 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M return "" % (self.id) - class RelatedUserStory(models.Model): user_story = models.ForeignKey("userstories.UserStory", on_delete=models.CASCADE) epic = models.ForeignKey("epics.Epic", on_delete=models.CASCADE) diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py index ff114ac6..a4ae20ec 100644 --- a/taiga/projects/references/api.py +++ b/taiga/projects/references/api.py @@ -45,6 +45,9 @@ class ResolverViewSet(viewsets.ViewSet): result = {"project": project.pk} + if data["epic"] and user_has_perm(request.user, "view_epics", project): + result["epic"] = get_object_or_404(project.epics.all(), + ref=data["epic"]).pk if data["us"] and user_has_perm(request.user, "view_us", project): result["us"] = get_object_or_404(project.user_stories.all(), ref=data["us"]).pk @@ -63,6 +66,11 @@ class ResolverViewSet(viewsets.ViewSet): if data["ref"]: ref_found = False # No need to continue once one ref is found + if ref_found is False and user_has_perm(request.user, "view_epics", project): + epic = project.epics.filter(ref=data["ref"]).first() + if epic: + result["epic"] = epic.pk + ref_found = True if user_has_perm(request.user, "view_us", project): us = project.user_stories.filter(ref=data["ref"]).first() if us: diff --git a/taiga/projects/references/validators.py b/taiga/projects/references/validators.py index 85456c4c..e91adb21 100644 --- a/taiga/projects/references/validators.py +++ b/taiga/projects/references/validators.py @@ -24,6 +24,7 @@ from taiga.base.exceptions import ValidationError class ResolverValidator(validators.Validator): project = serializers.CharField(max_length=512, required=True) milestone = serializers.CharField(max_length=512, required=False) + epic = serializers.IntegerField(required=False) us = serializers.IntegerField(required=False) task = serializers.IntegerField(required=False) issue = serializers.IntegerField(required=False) @@ -32,6 +33,8 @@ class ResolverValidator(validators.Validator): def validate(self, attrs): if "ref" in attrs: + if "epic" in attrs: + raise ValidationError("'epic' param is incompatible with 'ref' in the same request") if "us" in attrs: raise ValidationError("'us' param is incompatible with 'ref' in the same request") if "task" in attrs: From bb1a43e63731faa0fea1bfab02efbe163207d662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 12:10:49 +0200 Subject: [PATCH 173/261] Fix print --- taiga/timeline/management/commands/rebuild_timeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/timeline/management/commands/rebuild_timeline.py b/taiga/timeline/management/commands/rebuild_timeline.py index d3772a4f..674f6b9d 100644 --- a/taiga/timeline/management/commands/rebuild_timeline.py +++ b/taiga/timeline/management/commands/rebuild_timeline.py @@ -119,7 +119,7 @@ def generate_timeline(initial_date, final_date, project_id): _push_to_timelines(project, membership.user, membership, "create", membership.created_at) for project in projects.iterator(): - print("Project:", bulk_creator.created) + print("Project:", project) extra_data = { "values_diff": {}, "user": extract_user_info(project.owner), From d104e12b014749c2e6e97cd1d9461ebc11b1dbb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 12:12:14 +0200 Subject: [PATCH 174/261] Add histori entries for epics --- .../management/commands/sample_data.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 12e3f4e1..00103c6e 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -534,17 +534,16 @@ class Command(BaseCommand): user__isnull=False)).user epic.save() - # TODO: Epic history - #take_snapshot(epic, - # comment=self.sd.paragraph(), - # user=epic.owner) - # + take_snapshot(epic, + comment=self.sd.paragraph(), + user=epic.owner) + # Add history entry - #epic.status=self.sd.db_object_from_queryset(project.epic_statuses.filter(is_closed=False)) - #epic.save() - #take_snapshot(epic, - # comment=self.sd.paragraph(), - # user=epic.owner) + epic.status=self.sd.db_object_from_queryset(project.epic_statuses.filter(is_closed=False)) + epic.save() + take_snapshot(epic, + comment=self.sd.paragraph(), + user=epic.owner) self.create_votes(epic) self.create_watchers(epic) @@ -561,6 +560,11 @@ class Command(BaseCommand): user_story=us, order=idx+1) + # Add history entry + take_snapshot(epic, + comment=self.sd.paragraph(), + user=epic.owner) + return epic def create_project(self, counter, is_private=None, blocked_code=None): From c3570e944675a29c994c59e9b1591788bda6f96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 13:19:36 +0200 Subject: [PATCH 175/261] Add test of epic history endpoint --- .../test_history_resources.py | 243 +++++++++++++++++- 1 file changed, 239 insertions(+), 4 deletions(-) diff --git a/tests/integration/resources_permissions/test_history_resources.py b/tests/integration/resources_permissions/test_history_resources.py index bf659f69..95d77928 100644 --- a/tests/integration/resources_permissions/test_history_resources.py +++ b/tests/integration/resources_permissions/test_history_resources.py @@ -101,9 +101,247 @@ def data(): ######################################################### -## User stories +## Epics ######################################################### +@pytest.fixture +def data_epic(data): + m = type("Models", (object,), {}) + m.public_epic = f.EpicFactory(project=data.public_project, ref=22) + m.public_history_entry = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.public_project, + comment="testing public", + key=make_key_from_model_object(m.public_epic), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + + m.private_epic1 = f.EpicFactory(project=data.private_project1, ref=26) + m.private_history_entry1 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project1, + comment="testing 1", + key=make_key_from_model_object(m.private_epic1), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + m.private_epic2 = f.EpicFactory(project=data.private_project2, ref=210) + m.private_history_entry2 = f.HistoryEntryFactory.create(type=HistoryType.change, + project=data.private_project2, + comment="testing 2", + key=make_key_from_model_object(m.private_epic2), + diff={}, + user={"pk": data.project_member_with_perms.pk}) + return m + + +def test_epic_history_retrieve(client, data, data_epic): + public_url = reverse('epic-history-detail', kwargs={"pk": data_epic.public_epic.pk}) + private_url1 = reverse('epic-history-detail', kwargs={"pk": data_epic.private_epic1.pk}) + private_url2 = reverse('epic-history-detail', kwargs={"pk": data_epic.private_epic2.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_action_edit_comment(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-edit-comment', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-edit-comment', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-edit-comment', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + data = json.dumps({"comment": "testing update comment"}) + + results = helper_test_http_method(client, 'post', public_url, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, data, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_action_delete_comment(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-delete-comment', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-delete-comment', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-delete-comment', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_epic.public_history_entry.delete_comment_date = None + data_epic.public_history_entry.delete_comment_user = None + data_epic.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry1.delete_comment_date = None + data_epic.private_history_entry1.delete_comment_user = None + data_epic.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry2.delete_comment_date = None + data_epic.private_history_entry2.delete_comment_user = None + data_epic.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_epic_action_undelete_comment(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-undelete-comment', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-undelete-comment', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-undelete-comment', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users_and_statuses = [ + (None, 401), + (data.registered_user, 403), + (data.project_member_without_perms, 403), + (data.project_member_with_perms, 200), + (data.project_owner, 200), + ] + + for user, status_code in users_and_statuses: + data_epic.public_history_entry.delete_comment_date = timezone.now() + data_epic.public_history_entry.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_epic.public_history_entry.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(public_url) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry1.delete_comment_date = timezone.now() + data_epic.private_history_entry1.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_epic.private_history_entry1.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url1) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + for user, status_code in users_and_statuses: + data_epic.private_history_entry2.delete_comment_date = timezone.now() + data_epic.private_history_entry2.delete_comment_user = {"pk": data.project_member_with_perms.pk} + data_epic.private_history_entry2.save() + + if user: + client.login(user) + else: + client.logout() + response = client.json.post(private_url2) + error_mesage = "{} != {} for {}".format(response.status_code, status_code, user) + assert response.status_code == status_code, error_mesage + + +def test_epic_action_comment_versions(client, data, data_epic): + public_url = "{}?id={}".format( + reverse('epic-history-comment-versions', kwargs={"pk": data_epic.public_epic.pk}), + data_epic.public_history_entry.id + ) + private_url1 = "{}?id={}".format( + reverse('epic-history-comment-versions', kwargs={"pk": data_epic.private_epic1.pk}), + data_epic.private_history_entry1.id + ) + private_url2 = "{}?id={}".format( + reverse('epic-history-comment-versions', kwargs={"pk": data_epic.private_epic2.pk}), + data_epic.private_history_entry2.id + ) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner, + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + + +######################################################### +## User stories +######################################################### @pytest.fixture def data_us(data): @@ -344,7 +582,6 @@ def test_user_story_action_comment_versions(client, data, data_us): ## Tasks ######################################################### - @pytest.fixture def data_task(data): m = type("Models", (object,), {}) @@ -584,7 +821,6 @@ def test_task_action_comment_versions(client, data, data_task): ## Issues ######################################################### - @pytest.fixture def data_issue(data): m = type("Models", (object,), {}) @@ -824,7 +1060,6 @@ def test_issue_action_comment_versions(client, data, data_issue): ## Wiki pages ######################################################### - @pytest.fixture def data_wiki(data): m = type("Models", (object,), {}) From 79eeccf3799b5c3b716b3aeef89b9a45aa6cbbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 13:48:47 +0200 Subject: [PATCH 176/261] Add epic notifications --- taiga/projects/notifications/services.py | 3 +++ .../emails/epics/epic-change-body-html.jinja | 10 ++++++++++ .../emails/epics/epic-change-body-text.jinja | 8 ++++++++ .../templates/emails/epics/epic-change-subject.jinja | 3 +++ .../emails/epics/epic-create-body-html.jinja | 11 +++++++++++ .../emails/epics/epic-create-body-text.jinja | 8 ++++++++ .../templates/emails/epics/epic-create-subject.jinja | 3 +++ .../emails/epics/epic-delete-body-html.jinja | 11 +++++++++++ .../emails/epics/epic-delete-body-text.jinja | 8 ++++++++ .../templates/emails/epics/epic-delete-subject.jinja | 3 +++ 10 files changed, 68 insertions(+) create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja create mode 100644 taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index a4128622..4c7012d1 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -112,6 +112,7 @@ def _filter_by_permissions(obj, user): UserStory = apps.get_model("userstories", "UserStory") Issue = apps.get_model("issues", "Issue") Task = apps.get_model("tasks", "Task") + Epic = apps.get_model("epics", "Epic") WikiPage = apps.get_model("wiki", "WikiPage") if isinstance(obj, UserStory): @@ -120,6 +121,8 @@ def _filter_by_permissions(obj, user): return user_has_perm(user, "view_issues", obj, cache="project") elif isinstance(obj, Task): return user_has_perm(user, "view_tasks", obj, cache="project") + elif isinstance(obj, Epic): + return user_has_perm(user, "view_epics", obj, cache="project") elif isinstance(obj, WikiPage): return user_has_perm(user, "view_wiki_pages", obj, cache="project") return False diff --git a/taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja b/taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja new file mode 100644 index 00000000..5c84885d --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja @@ -0,0 +1,10 @@ +{% extends "emails/updates-body-html.jinja" %} + +{% block head %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %} +

Epic updated

+

Hello {{ user }},
{{ changer }} has updated a epic on {{ project }}

+

Epic #{{ ref }} {{ subject }}

+ See epic + {% endtrans %} +{% endblock %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja b/taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja new file mode 100644 index 00000000..1d6800e2 --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja @@ -0,0 +1,8 @@ +{% extends "emails/updates-body-text.jinja" %} +{% block head %} +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %} +Epic updated +Hello {{ user }}, {{ changer }} has updated a epic on {{ project }} +See epic #{{ ref }} {{ subject }} at {{ url }} +{% endtrans %} +{% endblock %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja b/taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja new file mode 100644 index 00000000..d66464e0 --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +[{{ project }}] Updated the epic #{{ ref }} "{{ subject }}" +{% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja b/taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja new file mode 100644 index 00000000..0484ee0b --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja @@ -0,0 +1,11 @@ +{% extends "emails/base-body-html.jinja" %} + +{% block body %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %} +

New epic created

+

Hello {{ user }},
{{ changer }} has created a new epic on {{ project }}

+

Epic #{{ ref }} {{ subject }}

+ See epic +

The Taiga Team

+ {% endtrans %} +{% endblock %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja b/taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja new file mode 100644 index 00000000..51748107 --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja @@ -0,0 +1,8 @@ +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject, url=resolve_front_url("epic", project.slug, snapshot.ref) %} +New epic created +Hello {{ user }}, {{ changer }} has created a new epic on {{ project }} +See epic #{{ ref }} {{ subject }} at {{ url }} + +--- +The Taiga Team +{% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja b/taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja new file mode 100644 index 00000000..d41e9c78 --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +[{{ project }}] Created the epic #{{ ref }} "{{ subject }}" +{% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja new file mode 100644 index 00000000..0debb545 --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja @@ -0,0 +1,11 @@ +{% extends "emails/base-body-html.jinja" %} + +{% block body %} + {% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +

Epic deleted

+

Hello {{ user }},
{{ changer }} has deleted a epic on {{ project }}

+

Epic #{{ ref }} {{ subject }}

+

The Taiga Team

+ {% endtrans %} +{% endblock %} + diff --git a/taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja new file mode 100644 index 00000000..b5855eba --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja @@ -0,0 +1,8 @@ +{% trans user=user.get_full_name(), changer=changer.get_full_name(), project=project.name, ref=snapshot.ref, subject=snapshot.subject %} +Epic deleted +Hello {{ user }}, {{ changer }} has deleted a epic on {{ project }} +Epic #{{ ref }} {{ subject }} + +--- +The Taiga Team +{% endtrans %} diff --git a/taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja b/taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja new file mode 100644 index 00000000..65286ec2 --- /dev/null +++ b/taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja @@ -0,0 +1,3 @@ +{% trans project=project.name|safe, ref=snapshot.ref, subject=snapshot.subject|safe %} +[{{ project }}] Deleted the epic #{{ ref }} "{{ subject }}" +{% endtrans %} From 0a5ba7b3ae21b4f380e2effbc006cd29c6d9a008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 20 Jul 2016 20:47:54 +0200 Subject: [PATCH 177/261] Add csv for Epics --- taiga/front/urls.py | 3 + taiga/projects/api.py | 36 +++--- taiga/projects/epics/api.py | 22 ++-- taiga/projects/epics/services.py | 111 ++++++++---------- .../migrations/0050_project_epics_csv_uuid.py | 20 ++++ taiga/projects/models.py | 2 + taiga/projects/permissions.py | 1 + taiga/projects/serializers.py | 9 +- .../test_epics_resources.py | 66 +++++------ .../test_projects_resource.py | 25 ++++ tests/integration/test_epics.py | 68 +++++++++++ 11 files changed, 241 insertions(+), 122 deletions(-) create mode 100644 taiga/projects/migrations/0050_project_epics_csv_uuid.py create mode 100644 tests/integration/test_epics.py diff --git a/taiga/front/urls.py b/taiga/front/urls.py index ab1cec8c..77d53dab 100644 --- a/taiga/front/urls.py +++ b/taiga/front/urls.py @@ -33,6 +33,9 @@ urls = { "project": "/project/{0}", # project.slug + "epics": "/project/{0}/epics/", # project.slug + "epic": "/project/{0}/epic/{1}", # project.slug, epic.ref + "backlog": "/project/{0}/backlog/", # project.slug "taskboard": "/project/{0}/taskboard/{1}", # project.slug, milestone.slug "kanban": "/project/{0}/kanban/", # project.slug diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 34920df4..5284f44e 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -243,6 +243,20 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, services.remove_user_from_project(request.user, project) return response.Ok() + def _regenerate_csv_uuid(self, project, field): + uuid_value = uuid.uuid4().hex + setattr(project, field, uuid_value) + project.save() + return uuid_value + + @detail_route(methods=["POST"]) + def regenerate_epics_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_epics_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "epics_csv_uuid")} + return response.Ok(data) + @detail_route(methods=["POST"]) def regenerate_userstories_csv_uuid(self, request, pk=None): project = self.get_object() @@ -251,14 +265,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")} return response.Ok(data) - @detail_route(methods=["POST"]) - def regenerate_issues_csv_uuid(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "regenerate_issues_csv_uuid", project) - self.pre_conditions_on_save(project) - data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} - return response.Ok(data) - @detail_route(methods=["POST"]) def regenerate_tasks_csv_uuid(self, request, pk=None): project = self.get_object() @@ -267,6 +273,14 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")} return response.Ok(data) + @detail_route(methods=["POST"]) + def regenerate_issues_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_issues_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} + return response.Ok(data) + @list_route(methods=["GET"]) def by_slug(self, request, *args, **kwargs): slug = request.QUERY_PARAMS.get("slug", None) @@ -293,12 +307,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "stats", project) return response.Ok(services.get_stats_for_project(project)) - def _regenerate_csv_uuid(self, project, field): - uuid_value = uuid.uuid4().hex - setattr(project, field, uuid_value) - project.save() - return uuid_value - @detail_route(methods=["GET"]) def member_stats(self, request, pk=None): project = self.get_object() diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index e71106d1..2c2760ba 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -154,18 +154,18 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return self.retrieve(request, **retrieve_kwargs) - #@list_route(methods=["GET"]) - #def csv(self, request): - # uuid = request.QUERY_PARAMS.get("uuid", None) - # if uuid is None: - # return response.NotFound() + @list_route(methods=["GET"]) + def csv(self, request): + uuid = request.QUERY_PARAMS.get("uuid", None) + if uuid is None: + return response.NotFound() - # project = get_object_or_404(Project, epics_csv_uuid=uuid) - # queryset = project.epics.all().order_by('ref') - # data = services.epics_to_csv(project, queryset) - # csv_response = HttpResponse(data.getvalue(), content_type='application/csv; charset=utf-8') - # csv_response['Content-Disposition'] = 'attachment; filename="epics.csv"' - # return csv_response + project = get_object_or_404(Project, epics_csv_uuid=uuid) + queryset = project.epics.all().order_by('ref') + data = services.epics_to_csv(project, queryset) + csv_response = HttpResponse(data.getvalue(), content_type='application/csv; charset=utf-8') + csv_response['Content-Disposition'] = 'attachment; filename="epics.csv"' + return csv_response @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index 01a0f0fa..8674e79f 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -105,66 +105,57 @@ def snapshot_epics_in_bulk(bulk_data, user): ##################################################### # CSV ##################################################### -# -#def epics_to_csv(project, queryset): -# csv_data = io.StringIO() -# fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start", -# "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to", -# "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order", -# "epicboard_order", "attachments", "external_reference", "tags", "watchers", "voters", -# "created_date", "modified_date", "finished_date"] -# -# custom_attrs = project.epiccustomattributes.all() -# for custom_attr in custom_attrs: -# fieldnames.append(custom_attr.name) -# -# queryset = queryset.prefetch_related("attachments", -# "custom_attributes_values") -# queryset = queryset.select_related("milestone", -# "owner", -# "assigned_to", -# "status", -# "project") -# -# queryset = attach_total_voters_to_queryset(queryset) -# queryset = attach_watchers_to_queryset(queryset) -# -# writer = csv.DictWriter(csv_data, fieldnames=fieldnames) -# writer.writeheader() -# for epic in queryset: -# epic_data = { -# "ref": epic.ref, -# "subject": epic.subject, -# "description": epic.description, -# "user_story": epic.user_story.ref if epic.user_story else None, -# "sprint": epic.milestone.name if epic.milestone else None, -# "sprint_estimated_start": epic.milestone.estimated_start if epic.milestone else None, -# "sprint_estimated_finish": epic.milestone.estimated_finish if epic.milestone else None, -# "owner": epic.owner.username if epic.owner else None, -# "owner_full_name": epic.owner.get_full_name() if epic.owner else None, -# "assigned_to": epic.assigned_to.username if epic.assigned_to else None, -# "assigned_to_full_name": epic.assigned_to.get_full_name() if epic.assigned_to else None, -# "status": epic.status.name if epic.status else None, -# "is_iocaine": epic.is_iocaine, -# "is_closed": epic.status is not None and epic.status.is_closed, -# "us_order": epic.us_order, -# "epicboard_order": epic.epicboard_order, -# "attachments": epic.attachments.count(), -# "external_reference": epic.external_reference, -# "tags": ",".join(epic.tags or []), -# "watchers": epic.watchers, -# "voters": epic.total_voters, -# "created_date": epic.created_date, -# "modified_date": epic.modified_date, -# "finished_date": epic.finished_date, -# } -# for custom_attr in custom_attrs: -# value = epic.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) -# epic_data[custom_attr.name] = value -# -# writer.writerow(epic_data) -# -# return csv_data + +def epics_to_csv(project, queryset): + csv_data = io.StringIO() + fieldnames = ["ref", "subject", "description", "owner", "owner_full_name", "assigned_to", + "assigned_to_full_name", "status", "epics_order", "client_requirement", + "team_requirement", "attachments", "tags", "watchers", "voters", + "created_date", "modified_date"] + + custom_attrs = project.epiccustomattributes.all() + for custom_attr in custom_attrs: + fieldnames.append(custom_attr.name) + + queryset = queryset.prefetch_related("attachments", + "custom_attributes_values") + queryset = queryset.select_related("owner", + "assigned_to", + "status", + "project") + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + + writer = csv.DictWriter(csv_data, fieldnames=fieldnames) + writer.writeheader() + for epic in queryset: + epic_data = { + "ref": epic.ref, + "subject": epic.subject, + "description": epic.description, + "owner": epic.owner.username if epic.owner else None, + "owner_full_name": epic.owner.get_full_name() if epic.owner else None, + "assigned_to": epic.assigned_to.username if epic.assigned_to else None, + "assigned_to_full_name": epic.assigned_to.get_full_name() if epic.assigned_to else None, + "status": epic.status.name if epic.status else None, + "epics_order": epic.epics_order, + "client_requirement": epic.client_requirement, + "team_requirement": epic.team_requirement, + "attachments": epic.attachments.count(), + "tags": ",".join(epic.tags or []), + "watchers": epic.watchers, + "voters": epic.total_voters, + "created_date": epic.created_date, + "modified_date": epic.modified_date, + } + for custom_attr in custom_attrs: + value = epic.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) + epic_data[custom_attr.name] = value + + writer.writerow(epic_data) + + return csv_data ##################################################### diff --git a/taiga/projects/migrations/0050_project_epics_csv_uuid.py b/taiga/projects/migrations/0050_project_epics_csv_uuid.py new file mode 100644 index 00000000..2dc87674 --- /dev/null +++ b/taiga/projects/migrations/0050_project_epics_csv_uuid.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-20 17:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0049_auto_20160629_1443'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='epics_csv_uuid', + field=models.CharField(blank=True, db_index=True, default=None, editable=False, max_length=32, null=True), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 02d24519..080cd411 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -202,6 +202,8 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): looking_for_people_note = models.TextField(default="", null=False, blank=True, verbose_name=_("loking for people note")) + epics_csv_uuid = models.CharField(max_length=32, editable=False, null=True, + blank=True, default=None, db_index=True) userstories_csv_uuid = models.CharField(max_length=32, editable=False, null=True, blank=True, default=None, db_index=True) diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 5e3b44db..7c10b5c2 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -62,6 +62,7 @@ class ProjectPermission(TaigaResourcePermission): stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project') issues_stats_perms = HasProjectPerm('view_project') + regenerate_epics_csv_uuid_perms = IsProjectAdmin() regenerate_userstories_csv_uuid_perms = IsProjectAdmin() regenerate_issues_csv_uuid_perms = IsProjectAdmin() regenerate_tasks_csv_uuid_perms = IsProjectAdmin() diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 58999578..a560ed36 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -373,9 +373,10 @@ class ProjectDetailSerializer(ProjectSerializer): # Admin fields is_private_extra_info = MethodField() max_memberships = MethodField() - issues_csv_uuid = Field() - tasks_csv_uuid = Field() + epics_csv_uuid = Field() userstories_csv_uuid = Field() + tasks_csv_uuid = Field() + issues_csv_uuid = Field() transfer_token = Field() milestones = MethodField() @@ -404,8 +405,8 @@ class ProjectDetailSerializer(ProjectSerializer): ret = super().to_value(instance) admin_fields = [ - "is_private_extra_info", "max_memberships", "issues_csv_uuid", - "tasks_csv_uuid", "userstories_csv_uuid", "transfer_token" + "epics_csv_uuid", "userstories_csv_uuid", "tasks_csv_uuid", "issues_csv_uuid", + "is_private_extra_info", "max_memberships", "transfer_token", ] is_admin_user = False diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index ceffe206..f4e33813 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -59,29 +59,29 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - owner=m.project_owner) - #epics_csv_uuid=uuid.uuid4().hex) + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - owner=m.project_owner) - #epics_csv_uuid=uuid.uuid4().hex) + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) - #epics_csv_uuid=uuid.uuid4().hex) + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, - #epics_csv_uuid=uuid.uuid4().hex, + epics_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) @@ -873,29 +873,29 @@ def test_epic_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -#def test_epics_csv(client, data): -# url = reverse('epics-csv') -# csv_public_uuid = data.public_project.epics_csv_uuid -# csv_private1_uuid = data.private_project1.epics_csv_uuid -# csv_private2_uuid = data.private_project1.epics_csv_uuid -# csv_blocked_uuid = data.blocked_project.epics_csv_uuid -# -# users = [ -# None, -# data.registered_user, -# data.project_member_without_perms, -# data.project_member_with_perms, -# data.project_owner -# ] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] +def test_epics_csv(client, data): + url = reverse('epics-csv') + csv_public_uuid = data.public_project.epics_csv_uuid + csv_private1_uuid = data.private_project1.epics_csv_uuid + csv_private2_uuid = data.private_project1.epics_csv_uuid + csv_blocked_uuid = data.blocked_project.epics_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 1410c86d..3d62dc6e 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -486,6 +486,31 @@ def test_invitations_retrieve(client, data): assert results == [200, 200, 200, 200] +def test_regenerate_epics_csv_uuid(client, data): + public_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 403, 403, 200] + + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 403, 403, 200] + + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 403, 200] + + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 403, 451] + + def test_regenerate_userstories_csv_uuid(client, data): public_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project1.pk}) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py new file mode 100644 index 00000000..5f22f62f --- /dev/null +++ b/tests/integration/test_epics.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import uuid +import csv + +from unittest import mock + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.epics import services + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_get_invalid_csv(client): + url = reverse("epics-csv") + + response = client.get(url) + assert response.status_code == 404 + + response = client.get("{}?uuid={}".format(url, "not-valid-uuid")) + assert response.status_code == 404 + + +def test_get_valid_csv(client): + url = reverse("epics-csv") + project = f.ProjectFactory.create(epics_csv_uuid=uuid.uuid4().hex) + + response = client.get("{}?uuid={}".format(url, project.epics_csv_uuid)) + assert response.status_code == 200 + + +def test_custom_fields_csv_generation(): + project = f.ProjectFactory.create(epics_csv_uuid=uuid.uuid4().hex) + attr = f.EpicCustomAttributeFactory.create(project=project, name="attr1", description="desc") + epic = f.EpicFactory.create(project=project) + attr_values = epic.custom_attributes_values + attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.save() + queryset = project.epics.all() + data = services.epics_to_csv(project, queryset) + data.seek(0) + reader = csv.reader(data) + row = next(reader) + assert row[17] == attr.name + row = next(reader) + assert row[17] == "val1" From 5a5d393815cdac563069f0501038effd6280a752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 21 Jul 2016 12:57:24 +0200 Subject: [PATCH 178/261] Add epic references signals --- taiga/projects/references/models.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/taiga/projects/references/models.py b/taiga/projects/references/models.py index 40aea018..61097ecb 100644 --- a/taiga/projects/references/models.py +++ b/taiga/projects/references/models.py @@ -21,10 +21,11 @@ from django.utils import timezone from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey +from taiga.projects.models import Project +from taiga.projects.epics.models import Epic from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue -from taiga.projects.models import Project from . import sequences as seq @@ -103,11 +104,22 @@ def attach_sequence(sender, instance, created, **kwargs): instance.save(update_fields=['ref']) +# Project models.signals.post_save.connect(create_sequence, sender=Project, dispatch_uid="refproj") -models.signals.pre_save.connect(store_previous_project, sender=UserStory, dispatch_uid="refus") -models.signals.pre_save.connect(store_previous_project, sender=Issue, dispatch_uid="refissue") -models.signals.pre_save.connect(store_previous_project, sender=Task, dispatch_uid="reftask") -models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid="refus") -models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue") -models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask") models.signals.post_delete.connect(delete_sequence, sender=Project, dispatch_uid="refprojdel") + +# Epic +models.signals.pre_save.connect(store_previous_project, sender=Epic, dispatch_uid="refepic") +models.signals.post_save.connect(attach_sequence, sender=Epic, dispatch_uid="refepic") + +# User Story +models.signals.pre_save.connect(store_previous_project, sender=UserStory, dispatch_uid="refus") +models.signals.post_save.connect(attach_sequence, sender=UserStory, dispatch_uid="refus") + +# Task +models.signals.pre_save.connect(store_previous_project, sender=Task, dispatch_uid="reftask") +models.signals.post_save.connect(attach_sequence, sender=Task, dispatch_uid="reftask") + +# Issue +models.signals.pre_save.connect(store_previous_project, sender=Issue, dispatch_uid="refissue") +models.signals.post_save.connect(attach_sequence, sender=Issue, dispatch_uid="refissue") From d307318b9012e1a9644f85ae8303d2ec2d279d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 21 Jul 2016 19:00:20 +0200 Subject: [PATCH 179/261] Add user story counts in epic serializer --- taiga/projects/epics/serializers.py | 5 +++++ taiga/projects/epics/utils.py | 17 ++++++++++++++++- .../projects/management/commands/sample_data.py | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py index 4118553e..081245d9 100644 --- a/taiga/projects/epics/serializers.py +++ b/taiga/projects/epics/serializers.py @@ -49,10 +49,15 @@ class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, blocked_note = Field() tags = Field() is_closed = MethodField() + user_stories_counts = MethodField() def get_is_closed(self, obj): return obj.status is not None and obj.status.is_closed + def get_user_stories_counts(self, obj): + assert hasattr(obj, "user_stories_counts"), "instance must have a user_stories_counts attribute" + return obj.user_stories_counts + class EpicSerializer(EpicListSerializer): comment = MethodField() diff --git a/taiga/projects/epics/utils.py b/taiga/projects/epics/utils.py index d10dddab..20f45046 100644 --- a/taiga/projects/epics/utils.py +++ b/taiga/projects/epics/utils.py @@ -26,14 +26,29 @@ from taiga.projects.votes.utils import attach_is_voter_to_queryset def attach_extra_info(queryset, user=None, include_attachments=False): - if include_attachments: queryset = attach_basic_attachments(queryset) queryset = queryset.extra(select={"include_attachments": "True"}) + queryset = attach_user_stories_counts_to_queryset(queryset) queryset = attach_total_voters_to_queryset(queryset) queryset = attach_watchers_to_queryset(queryset) queryset = attach_total_watchers_to_queryset(queryset) queryset = attach_is_voter_to_queryset(queryset, user) queryset = attach_is_watcher_to_queryset(queryset, user) return queryset + + +def attach_user_stories_counts_to_queryset(queryset, as_field="user_stories_counts"): + model = queryset.model + sql = """SELECT json_build_object( + 'opened', COALESCE(SUM(CASE WHEN is_closed IS FALSE THEN 1 ELSE 0 END), 0), + 'closed', COALESCE(SUM(CASE WHEN is_closed IS TRUE THEN 1 ELSE 0 END), 0) + ) + FROM epics_relateduserstory + INNER JOIN userstories_userstory ON epics_relateduserstory.user_story_id = userstories_userstory.id + WHERE epics_relateduserstory.epic_id = {tbl}.id""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 00103c6e..54b4e027 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -111,7 +111,7 @@ NUM_EMPTY_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_EMPTY_PROJECTS", 2) NUM_BLOCKED_PROJECTS = getattr(settings, "SAMPLE_DATA_NUM_BLOCKED_PROJECTS", 1) NUM_MILESTONES = getattr(settings, "SAMPLE_DATA_NUM_MILESTONES", (1, 5)) NUM_EPICS = getattr(settings, "SAMPLE_DATA_NUM_EPICS", (4, 8)) -NUM_USS_EPICS = getattr(settings, "SAMPLE_DATA_NUM_USS_EPICS", (2, 6)) +NUM_USS_EPICS = getattr(settings, "SAMPLE_DATA_NUM_USS_EPICS", (2, 12)) NUM_USS = getattr(settings, "SAMPLE_DATA_NUM_USS", (3, 7)) NUM_TASKS_FINISHED = getattr(settings, "SAMPLE_DATA_NUM_TASKS_FINISHED", (1, 5)) NUM_TASKS = getattr(settings, "SAMPLE_DATA_NUM_TASKS", (0, 4)) @@ -553,7 +553,7 @@ class Command(BaseCommand): if self.sd.choice([True, True, False, True, True]): filters = {"project": epic.project} n = self.sd.choice(list(range(self.sd.int(*NUM_USS_EPICS)))) - + print (n) user_stories = UserStory.objects.filter(**filters).order_by("?")[:n] for idx, us in enumerate(list(user_stories)): RelatedUserStory.objects.create(epic=epic, From 9a1b422a42fdb6eae2e0527341f78cd904ab2acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 21 Jul 2016 20:06:39 +0200 Subject: [PATCH 180/261] Add project extra info to the user story serializer --- taiga/projects/mixins/serializers.py | 29 +++++++++++++++++++++-- taiga/projects/serializers.py | 12 ++++++---- taiga/projects/userstories/serializers.py | 7 +++--- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index 945c1119..cec50b2b 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -16,12 +16,13 @@ # 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.base.api import serializers from taiga.base.fields import Field, MethodField +from taiga.projects import services from taiga.users.serializers import UserBasicInfoSerializer -from django.utils.translation import ugettext as _ - class CachedUsersSerializerMixin(serializers.LightSerializer): def to_value(self, instance): @@ -77,3 +78,27 @@ class StatusExtraInfoSerializerMixin(serializers.LightSerializer): self._serialized_status[obj.status_id] = serialized_status return serialized_status + + +class ProjectExtraInfoSerializerMixin(serializers.LightSerializer): + project = Field(attr="project_id") + project_extra_info = MethodField() + + def to_value(self, instance): + self._serialized_project = {} + return super().to_value(instance) + + def get_project_extra_info(self, obj): + if obj.project_id is None: + return None + + serialized_project = self._serialized_project.get(obj.project_id, None) + if serialized_project is None: + serialized_project = { + "name": obj.project.name, + "slug": obj.project.slug, + "logo_small_url": services.get_logo_small_thumbnail_url(obj.project) + } + self._serialized_project[obj.project_id] = serialized_project + + return serialized_project diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index a560ed36..eb7b2e54 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -434,8 +434,10 @@ class ProjectDetailSerializer(ProjectSerializer): return len(obj.members_attr) def get_is_out_of_owner_limits(self, obj): - assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" - assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" + assert hasattr(obj, "private_projects_same_owner_attr"), ("instance must have a private_projects_same" + "_owner_attr attribute") + assert hasattr(obj, "public_projects_same_owner_attr"), ("instance must have a public_projects_same_" + "owner_attr attribute") return services.check_if_project_is_out_of_owner_limits( obj, current_memberships=self.get_total_memberships(obj), @@ -444,8 +446,10 @@ class ProjectDetailSerializer(ProjectSerializer): ) def get_is_private_extra_info(self, obj): - assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" - assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" + assert hasattr(obj, "private_projects_same_owner_attr"), ("instance must have a private_projects_same_" + "owner_attr attribute") + assert hasattr(obj, "public_projects_same_owner_attr"), ("instance must have a public_projects_same" + "_owner_attr attribute") return services.check_if_project_privacity_can_be_changed( obj, current_memberships=self.get_total_memberships(obj), diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index abb10592..bc977a2e 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -22,11 +22,12 @@ from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin -from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer -from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import ProjectExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import WatchedResourceSerializer +from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin @@ -42,7 +43,7 @@ class OriginIssueSerializer(serializers.LightSerializer): return super().to_value(instance) -class UserStoryListSerializer( +class UserStoryListSerializer(ProjectExtraInfoSerializerMixin, VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, From fb673ab844f57ca3edee933788e4bd31e39c20b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 22 Jul 2016 12:19:56 +0200 Subject: [PATCH 181/261] Ability to order stories inside an Epic --- taiga/projects/userstories/api.py | 1 + .../migrations/0013_auto_20160722_1018.py | 25 +++++++++++++++++++ taiga/projects/userstories/models.py | 4 ++- taiga/projects/userstories/serializers.py | 1 + 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 taiga/projects/userstories/migrations/0013_auto_20160722_1018.py diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 3b6e108a..d391e08f 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -83,6 +83,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi order_by_fields = ["backlog_order", "sprint_order", "kanban_order", + "epic_order", "total_voters"] def get_serializer_class(self, *args, **kwargs): diff --git a/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py b/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py new file mode 100644 index 00000000..45e11419 --- /dev/null +++ b/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-22 10:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('userstories', '0012_auto_20160614_1201'), + ] + + operations = [ + migrations.AddField( + model_name='userstory', + name='epic_order', + field=models.IntegerField(default=10000, verbose_name='epic order'), + ), + migrations.AlterField( + model_name='userstory', + name='kanban_order', + field=models.IntegerField(default=10000, verbose_name='kanban order'), + ), + ] diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index d774e5b4..f096c885 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -80,7 +80,9 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod sprint_order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("sprint order")) kanban_order = models.IntegerField(null=False, blank=False, default=10000, - verbose_name=_("sprint order")) + verbose_name=_("kanban order")) + epic_order = models.IntegerField(null=False, blank=False, default=10000, + verbose_name=_("epic order")) created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index bc977a2e..f60fb2b6 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -61,6 +61,7 @@ class UserStoryListSerializer(ProjectExtraInfoSerializerMixin, backlog_order = Field() sprint_order = Field() kanban_order = Field() + epic_order = Field() created_date = Field() modified_date = Field() finish_date = Field() From 5cda117c1b26c280179de2bf3f1fba7d99c1af2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 22 Jul 2016 12:39:02 +0200 Subject: [PATCH 182/261] Remove deprecate code --- taiga/projects/epics/api.py | 9 +++------ taiga/projects/issues/api.py | 14 ++------------ taiga/projects/tasks/api.py | 5 ----- taiga/projects/userstories/api.py | 9 ++------- 4 files changed, 7 insertions(+), 30 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 2c2760ba..bbfd164f 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -53,12 +53,9 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, filters.StatusesFilter, filters.TagsFilter, filters.WatchersFilter, - filters.QFilter) - retrieve_exclude_filters = (filters.OwnersFilter, - filters.AssignedToFilter, - filters.StatusesFilter, - filters.TagsFilter, - filters.WatchersFilter) + filters.QFilter, + filters.CreatedDateFilter, + filters.ModifiedDateFilter) filter_fields = ["project", "project__slug", "assigned_to", diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 08533e24..617b13ec 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -58,23 +58,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W filters.TagsFilter, filters.WatchersFilter, filters.QFilter, - filters.OrderByFilterMixin, filters.CreatedDateFilter, filters.ModifiedDateFilter, - filters.FinishedDateFilter) - retrieve_exclude_filters = (filters.OwnersFilter, - filters.AssignedToFilter, - filters.StatusesFilter, - filters.IssueTypesFilter, - filters.SeveritiesFilter, - filters.PrioritiesFilter, - filters.TagsFilter, - filters.WatchersFilter,) - + filters.FinishedDateFilter, + filters.OrderByFilterMixin) filter_fields = ("project", "project__slug", "status__is_closed") - order_by_fields = ("type", "status", "severity", diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 3cf43913..bc76f24f 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -60,11 +60,6 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, filters.CreatedDateFilter, filters.ModifiedDateFilter, filters.FinishedDateFilter) - retrieve_exclude_filters = (filters.OwnersFilter, - filters.AssignedToFilter, - filters.StatusesFilter, - filters.TagsFilter, - filters.WatchersFilter) filter_fields = ["user_story", "milestone", "project", diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index d391e08f..32b798a1 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -64,15 +64,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi filters.TagsFilter, filters.WatchersFilter, filters.QFilter, - filters.OrderByFilterMixin, filters.CreatedDateFilter, filters.ModifiedDateFilter, - filters.FinishDateFilter) - retrieve_exclude_filters = (filters.OwnersFilter, - filters.AssignedToFilter, - filters.StatusesFilter, - filters.TagsFilter, - filters.WatchersFilter) + filters.FinishDateFilter, + filters.OrderByFilterMixin) filter_fields = ["project", "project__slug", "milestone", From ff7d241a3755762795bdb2687095d58f7e1ad1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 26 Jul 2016 13:05:19 +0200 Subject: [PATCH 183/261] Filter userstories by epics --- taiga/projects/userstories/api.py | 36 +++++++------ taiga/projects/userstories/filters.py | 24 +++++++++ taiga/projects/userstories/services.py | 71 ++++++++++++++++++++++++- tests/factories.py | 13 ++++- tests/integration/test_userstories.py | 73 +++++++++++++++++--------- 5 files changed, 174 insertions(+), 43 deletions(-) create mode 100644 taiga/projects/userstories/filters.py diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 32b798a1..45d78f28 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -22,7 +22,7 @@ from django.db import transaction from django.utils.translation import ugettext as _ from django.http import HttpResponse -from taiga.base import filters +from taiga.base import filters as base_filters from taiga.base import exceptions as exc from taiga.base import response from taiga.base import status @@ -45,6 +45,7 @@ from taiga.projects.votes.mixins.viewsets import VotedResourceMixin from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin from taiga.projects.userstories.utils import attach_extra_info +from . import filters from . import models from . import permissions from . import serializers @@ -57,17 +58,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi validator_class = validators.UserStoryValidator queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) - filter_backends = (filters.CanViewUsFilterBackend, - filters.OwnersFilter, - filters.AssignedToFilter, - filters.StatusesFilter, - filters.TagsFilter, - filters.WatchersFilter, - filters.QFilter, - filters.CreatedDateFilter, - filters.ModifiedDateFilter, - filters.FinishDateFilter, - filters.OrderByFilterMixin) + filter_backends = (base_filters.CanViewUsFilterBackend, + filters.EpicsFilter, + base_filters.OwnersFilter, + base_filters.AssignedToFilter, + base_filters.StatusesFilter, + base_filters.TagsFilter, + base_filters.WatchersFilter, + base_filters.QFilter, + base_filters.CreatedDateFilter, + base_filters.ModifiedDateFilter, + base_filters.FinishDateFilter, + base_filters.OrderByFilterMixin) filter_fields = ["project", "project__slug", "milestone", @@ -270,16 +272,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi project = get_object_or_404(Project, id=project_id) filter_backends = self.get_filter_backends() - statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) - assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) - owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) + statuses_filter_backends = (f for f in filter_backends if f != base_filters.StatusesFilter) + assigned_to_filter_backends = (f for f in filter_backends if f != base_filters.AssignedToFilter) + owners_filter_backends = (f for f in filter_backends if f != base_filters.OwnersFilter) + epics_filter_backends = (f for f in filter_backends if f != filters.EpicsFilter) queryset = self.get_queryset() querysets = { "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), - "tags": self.filter_queryset(queryset) + "tags": self.filter_queryset(queryset), + "epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends) } return response.Ok(services.get_userstories_filters_data(project, querysets)) diff --git a/taiga/projects/userstories/filters.py b/taiga/projects/userstories/filters.py new file mode 100644 index 00000000..5f877618 --- /dev/null +++ b/taiga/projects/userstories/filters.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 taiga.base import filters + + +class EpicsFilter(filters.BaseRelatedFieldsFilter): + filter_name = 'epics' diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 0f260e2c..11fb2a2f 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -244,7 +244,7 @@ def userstories_to_csv(project, queryset): "tasks": ",".join([str(task.ref) for task in us.tasks.all()]), "tags": ",".join(us.tags or []), "watchers": us.watchers, - "voters": us.total_voters, + "voters": us.total_voters } us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()} @@ -456,6 +456,74 @@ def _get_userstories_tags(project, queryset): return sorted(result, key=itemgetter("name")) +def _get_userstories_epics(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH counters AS ( + SELECT "epics_relateduserstory"."epic_id" AS "epic_id", + count("epics_relateduserstory"."id") AS "counter" + FROM "epics_relateduserstory" + INNER JOIN "userstories_userstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + WHERE {where} + GROUP BY "epics_relateduserstory"."epic_id" + ) + SELECT "epics_epic"."id" AS "id", + "epics_epic"."ref" AS "ref", + "epics_epic"."subject" AS "subject", + "epics_epic"."epics_order" AS "order", + COALESCE("counters"."counter", 0) AS "counter" + FROM "epics_epic" + LEFT OUTER JOIN "counters" + ON ("counters"."epic_id" = "epics_epic"."id") + WHERE "epics_epic"."project_id" = %s + + -- User stories with no epics (return results only if there are userstories) + UNION + SELECT NULL AS "id", + NULL AS "ref", + NULL AS "subject", + 0 AS "order", + count(COALESCE("epics_relateduserstory"."epic_id", -1)) AS "counter" + FROM "userstories_userstory" + LEFT OUTER JOIN "epics_relateduserstory" + ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id") + WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL + GROUP BY "epics_relateduserstory"."epic_id" + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id] + where_params) + rows = cursor.fetchall() + + result = [] + for id, ref, subject, order, count in rows: + result.append({ + "id": id, + "ref": ref, + "subject": subject, + "order": order, + "count": count, + }) + + result = sorted(result, key=itemgetter("order")) + + # Add row when there is no user stories with no epics + if result[0]["id"] is not None: + result.insert(0, { + "id": None, + "ref": None, + "subject": None, + "order": 0, + "count": 0, + }) + return result + + def get_userstories_filters_data(project, querysets): """ Given a project and an userstories queryset, return a simple data structure @@ -466,6 +534,7 @@ def get_userstories_filters_data(project, querysets): ("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])), ("owners", _get_userstories_owners(project, querysets["owners"])), ("tags", _get_userstories_tags(project, querysets["tags"])), + ("epics", _get_userstories_epics(project, querysets["epics"])), ]) return data diff --git a/tests/factories.py b/tests/factories.py index 2e831c13..5cec5800 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -249,11 +249,20 @@ class EpicFactory(Factory): ref = factory.Sequence(lambda n: n) project = factory.SubFactory("tests.factories.ProjectFactory") owner = factory.SubFactory("tests.factories.UserFactory") - subject = factory.Sequence(lambda n: "User Story {}".format(n)) - description = factory.Sequence(lambda n: "User Story {} description".format(n)) + subject = factory.Sequence(lambda n: "Epic {}".format(n)) + description = factory.Sequence(lambda n: "Epic {} description".format(n)) status = factory.SubFactory("tests.factories.EpicStatusFactory") +class RelatedUserStory(Factory): + class Meta: + model = "epics.RelatedUserStory" + strategy = factory.CREATE_STRATEGY + + epic = factory.SubFactory("tests.factories.EpicFactory") + user_story = factory.SubFactory("tests.factories.UserStoryFactory") + + class MilestoneFactory(Factory): class Meta: model = "milestones.Milestone" diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index c9cd4bb1..e158b802 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -625,45 +625,55 @@ def test_api_filters_data(client): status2 = f.UserStoryStatusFactory.create(project=project) status3 = f.UserStoryStatusFactory.create(project=project) + epic0 = f.EpicFactory.create(project=project) + epic1 = f.EpicFactory.create(project=project) + epic2 = f.EpicFactory.create(project=project) + tag0 = "test1test2test3" tag1 = "test1" tag2 = "test2" tag3 = "test3" - # ------------------------------------------------------ - # | US | Owner | Assigned To | Tags | - # |-------#--------#-------------#---------------------| - # | 0 | user2 | None | tag1 | - # | 1 | user1 | None | tag2 | - # | 2 | user3 | None | tag1 tag2 | - # | 3 | user2 | None | tag3 | - # | 4 | user1 | user1 | tag1 tag2 tag3 | - # | 5 | user3 | user1 | tag3 | - # | 6 | user2 | user1 | tag1 tag2 | - # | 7 | user1 | user2 | tag3 | - # | 8 | user3 | user2 | tag1 | - # | 9 | user2 | user3 | tag0 | - # ------------------------------------------------------ + # ------------------------------------------------------------------------------ + # | US | Status | Owner | Assigned To | Tags | Epic | + # |-------#---------#--------#-------------#---------------------#-------------- + # | 0 | status3 | user2 | None | tag1 | epic0 | + # | 1 | status3 | user1 | None | tag2 | None | + # | 2 | status1 | user3 | None | tag1 tag2 | epic1 | + # | 3 | status0 | user2 | None | tag3 | None | + # | 4 | status0 | user1 | user1 | tag1 tag2 tag3 | epic0 | + # | 5 | status2 | user3 | user1 | tag3 | None | + # | 6 | status3 | user2 | user1 | tag1 tag2 | epic0 epic2 | + # | 7 | status0 | user1 | user2 | tag3 | None | + # | 8 | status3 | user3 | user2 | tag1 | epic2 | + # | 9 | status1 | user2 | user3 | tag0 | none | + # ------------------------------------------------------------------------------ - f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, status=status3, tags=[tag1]) - f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, + f.RelatedUserStory.create(user_story=us0, epic=epic0) + us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, status=status3, tags=[tag2]) - f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, + us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, status=status1, tags=[tag1, tag2]) - f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + f.RelatedUserStory.create(user_story=us2, epic=epic1) + us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, status=status0, tags=[tag3]) - f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, + us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, status=status0, tags=[tag1, tag2, tag3]) - f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, + f.RelatedUserStory.create(user_story=us4, epic=epic0) + us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, status=status2, tags=[tag3]) - f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, + us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, status=status3, tags=[tag1, tag2]) - f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, + f.RelatedUserStory.create(user_story=us6, epic=epic0) + f.RelatedUserStory.create(user_story=us6, epic=epic2) + us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, status=status0, tags=[tag3]) - f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, + us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, status=status3, tags=[tag1]) - f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, + f.RelatedUserStory.create(user_story=us8, epic=epic2) + us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, status=status1, tags=[tag0]) url = reverse("userstories-filters-data") + "?project={}".format(project.id) @@ -693,6 +703,11 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + # Filter ((status0 or status3) response = client.get(url + "&status={},{}".format(status3.id, status0.id)) assert response.status_code == 200 @@ -716,6 +731,11 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + # Filter ((tag1 and tag2) and (user1 or user2)) response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) assert response.status_code == 200 @@ -739,6 +759,11 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 1 + def test_get_invalid_csv(client): url = reverse("userstories-csv") From f5f8f66b579670065ea4a70ecfd2e81f3eb81ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 27 Jul 2016 12:09:17 +0200 Subject: [PATCH 184/261] Add color to Epics --- taiga/base/utils/colors.py | 56 +++++++++++++++++++ .../epics/migrations/0002_epic_color.py | 21 +++++++ taiga/projects/epics/models.py | 4 ++ taiga/projects/epics/serializers.py | 1 + .../management/commands/sample_data.py | 1 - taiga/users/models.py | 5 +- 6 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 taiga/base/utils/colors.py create mode 100644 taiga/projects/epics/migrations/0002_epic_color.py diff --git a/taiga/base/utils/colors.py b/taiga/base/utils/colors.py new file mode 100644 index 00000000..517c8add --- /dev/null +++ b/taiga/base/utils/colors.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import random + +from django.conf import settings + + +DEFAULT_PREDEFINED_COLORS = ( + "#fce94f", + "#edd400", + "#c4a000", + "#8ae234", + "#73d216", + "#4e9a06", + "#d3d7cf", + "#fcaf3e", + "#f57900", + "#ce5c00", + "#729fcf", + "#3465a4", + "#204a87", + "#888a85", + "#ad7fa8", + "#75507b", + "#5c3566", + "#ef2929", + "#cc0000", + "#a40000" +) + +PREDEFINED_COLORS = getattr(settings, "PREDEFINED_COLORS", DEFAULT_PREDEFINED_COLORS) + + +def generate_random_hex_color(): + return "#{:06x}".format(random.randint(0,0xFFFFFF)) + + +def generate_random_predefined_hex_color(): + return random.choice(PREDEFINED_COLORS) + diff --git a/taiga/projects/epics/migrations/0002_epic_color.py b/taiga/projects/epics/migrations/0002_epic_color.py new file mode 100644 index 00000000..b9cd2ced --- /dev/null +++ b/taiga/projects/epics/migrations/0002_epic_color.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-27 09:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.colors + + +class Migration(migrations.Migration): + + dependencies = [ + ('epics', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='epic', + name='color', + field=models.CharField(blank=True, default=taiga.base.utils.colors.generate_random_predefined_hex_color, max_length=32, verbose_name='color'), + ), + ] diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index 75380556..6b7d9231 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -22,6 +22,7 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from taiga.base.utils.colors import generate_random_predefined_hex_color from taiga.projects.tagging.models import TaggedMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin @@ -51,6 +52,9 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M subject = models.TextField(null=False, blank=False, verbose_name=_("subject")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) + color = models.CharField(max_length=32, null=False, blank=True, + default=generate_random_predefined_hex_color, + verbose_name=_("color")) assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None, related_name="epics_assigned_to_me", verbose_name=_("assigned to")) diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py index 081245d9..9b845bbe 100644 --- a/taiga/projects/epics/serializers.py +++ b/taiga/projects/epics/serializers.py @@ -40,6 +40,7 @@ class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, created_date = Field() modified_date = Field() subject = Field() + color = Field() epics_order = Field() client_requirement = Field() team_requirement = Field() diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 54b4e027..b659096a 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -553,7 +553,6 @@ class Command(BaseCommand): if self.sd.choice([True, True, False, True, True]): filters = {"project": epic.project} n = self.sd.choice(list(range(self.sd.int(*NUM_USS_EPICS)))) - print (n) user_stories = UserStory.objects.filter(**filters).order_by("?")[:n] for idx, us in enumerate(list(user_stories)): RelatedUserStory.objects.create(epic=epic, diff --git a/taiga/users/models.py b/taiga/users/models.py index 98a71dc8..45908a10 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -38,6 +38,7 @@ from django_pgjson.fields import JsonField from django_pglocks import advisory_lock from taiga.auth.tokens import get_token_for_user +from taiga.base.utils.colors import generate_random_hex_color from taiga.base.utils.slug import slugify_uniquely from taiga.base.utils.files import get_file_path from taiga.permissions.choices import MEMBERS_PERMISSIONS @@ -82,10 +83,6 @@ def get_user_model_safe(): raise -def generate_random_hex_color(): - return "#{:06x}".format(random.randint(0,0xFFFFFF)) - - def get_user_file_path(instance, filename): return get_file_path(instance, filename, "user") From b014c25454637fa90d5b3381d0547023a67c42b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 27 Jul 2016 13:25:31 +0200 Subject: [PATCH 185/261] Remove epic order from user story --- taiga/projects/userstories/api.py | 1 - .../userstories/migrations/0013_auto_20160722_1018.py | 5 ----- taiga/projects/userstories/models.py | 2 -- taiga/projects/userstories/serializers.py | 1 - 4 files changed, 9 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 45d78f28..23a471fb 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -80,7 +80,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi order_by_fields = ["backlog_order", "sprint_order", "kanban_order", - "epic_order", "total_voters"] def get_serializer_class(self, *args, **kwargs): diff --git a/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py b/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py index 45e11419..64a73be8 100644 --- a/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py +++ b/taiga/projects/userstories/migrations/0013_auto_20160722_1018.py @@ -12,11 +12,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name='userstory', - name='epic_order', - field=models.IntegerField(default=10000, verbose_name='epic order'), - ), migrations.AlterField( model_name='userstory', name='kanban_order', diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index f096c885..d0216d21 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -81,8 +81,6 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod verbose_name=_("sprint order")) kanban_order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("kanban order")) - epic_order = models.IntegerField(null=False, blank=False, default=10000, - verbose_name=_("epic order")) created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index f60fb2b6..bc977a2e 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -61,7 +61,6 @@ class UserStoryListSerializer(ProjectExtraInfoSerializerMixin, backlog_order = Field() sprint_order = Field() kanban_order = Field() - epic_order = Field() created_date = Field() modified_date = Field() finish_date = Field() From d461b1962d62e7fe3c8b348e830f80c7de7ad7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 27 Jul 2016 13:41:34 +0200 Subject: [PATCH 186/261] Add epics to user stories serializers --- taiga/projects/userstories/serializers.py | 6 ++++- taiga/projects/userstories/utils.py | 31 ++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index bc977a2e..97fcea42 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -77,9 +77,13 @@ class UserStoryListSerializer(ProjectExtraInfoSerializerMixin, total_points = MethodField() comment = MethodField() origin_issue = OriginIssueSerializer(attr="generated_from_issue") - + epics = MethodField() tasks = MethodField() + def get_epics(self, obj): + assert hasattr(obj, "epics_attr"), "instance must have a epics_attr attribute" + return obj.epics_attr + def get_milestone_slug(self, obj): return obj.milestone.slug if obj.milestone else None diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index 35456b71..ea7637f4 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -71,7 +71,7 @@ def attach_tasks(queryset, as_field="tasks_attr"): """Attach tasks as json column to each object of the queryset. :param queryset: A Django user stories queryset object. - :param as_field: Attach the role points as an attribute with this name. + :param as_field: Attach tasks as an attribute with this name. :return: Queryset object with the additional `as_field` field. """ @@ -99,9 +99,38 @@ def attach_tasks(queryset, as_field="tasks_attr"): return queryset +def attach_epics(queryset, as_field="epics_attr"): + """Attach epics as json column to each object of the queryset. + + :param queryset: A Django user stories queryset object. + :param as_field: Attach the epics as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + + model = queryset.model + sql = """SELECT json_agg(row_to_json(t)) + FROM (SELECT "epics_epic"."id" AS "id", + "epics_epic"."ref" AS "ref", + "epics_epic"."subject" AS "subject", + "epics_epic"."color" AS "color", + json_build_object('id', "projects_project"."id", + 'name', "projects_project"."name", + 'slug', "projects_project"."slug") AS "project" + FROM "epics_relateduserstory" + INNER JOIN "epics_epic" ON "epics_epic"."id" = "epics_relateduserstory"."epic_id" + INNER JOIN "projects_project" ON "projects_project"."id" = "epics_epic"."project_id" + WHERE "epics_relateduserstory"."user_story_id" = {tbl}.id) t""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False): queryset = attach_total_points(queryset) queryset = attach_role_points(queryset) + queryset = attach_epics(queryset) if include_attachments: queryset = attach_basic_attachments(queryset) From e2f6245fbbca7dc5e516bb7343ce10ee8420f0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 27 Jul 2016 19:00:32 +0200 Subject: [PATCH 187/261] Add epic order in userstories if filter by epic ide is enabled --- taiga/base/filters.py | 11 +++++++-- taiga/projects/userstories/api.py | 6 ++++- taiga/projects/userstories/filters.py | 7 +++++- taiga/projects/userstories/serializers.py | 12 +++++++++ taiga/projects/userstories/utils.py | 30 +++++++++++++++++++++-- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index a30e2dcf..06274128 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -337,10 +337,16 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi ##################################################################### class BaseRelatedFieldsFilter(FilterBackend): - def __init__(self, filter_name=None): + filter_name = None + param_name = None + + def __init__(self, filter_name=None, param_name=None): if filter_name: self.filter_name = filter_name + if param_name: + self.param_name = param_name + def _prepare_filter_data(self, query_param_value): def _transform_value(value): try: @@ -355,7 +361,8 @@ class BaseRelatedFieldsFilter(FilterBackend): return list(values) def _get_queryparams(self, params): - raw_value = params.get(self.filter_name, None) + param_name = self.param_name or self.filter_name + raw_value = params.get(param_name, None) if raw_value: value = self._prepare_filter_data(raw_value) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 23a471fb..758fa98c 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -60,6 +60,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi permission_classes = (permissions.UserStoryPermission,) filter_backends = (base_filters.CanViewUsFilterBackend, filters.EpicsFilter, + filters.EpicFilter, base_filters.OwnersFilter, base_filters.AssignedToFilter, base_filters.StatusesFilter, @@ -80,6 +81,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi order_by_fields = ["backlog_order", "sprint_order", "kanban_order", + "epic_order", "total_voters"] def get_serializer_class(self, *args, **kwargs): @@ -102,9 +104,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi include_attachments = "include_attachments" in self.request.QUERY_PARAMS include_tasks = "include_tasks" in self.request.QUERY_PARAMS + epic_id = self.request.QUERY_PARAMS.get("epic", None) qs = attach_extra_info(qs, user=self.request.user, include_attachments=include_attachments, - include_tasks=include_tasks) + include_tasks=include_tasks, + epic_id=epic_id) return qs diff --git a/taiga/projects/userstories/filters.py b/taiga/projects/userstories/filters.py index 5f877618..f061667e 100644 --- a/taiga/projects/userstories/filters.py +++ b/taiga/projects/userstories/filters.py @@ -20,5 +20,10 @@ from taiga.base import filters +class EpicFilter(filters.BaseRelatedFieldsFilter): + filter_name = "epics" + param_name = "epic" + + class EpicsFilter(filters.BaseRelatedFieldsFilter): - filter_name = 'epics' + filter_name = "epics" diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 97fcea42..b7c43466 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -78,8 +78,20 @@ class UserStoryListSerializer(ProjectExtraInfoSerializerMixin, comment = MethodField() origin_issue = OriginIssueSerializer(attr="generated_from_issue") epics = MethodField() + epic_order = MethodField() tasks = MethodField() + def get_epic_order(self, obj): + include_epic_order = getattr(obj, "include_epic_order", False) + + if include_epic_order: + assert hasattr(obj, "epic_order"), "instance must have a epic_order attribute" + + if not include_epic_order or obj.epic_order is None: + return None + + return obj.epic_order + def get_epics(self, obj): assert hasattr(obj, "epics_attr"), "instance must have a epics_attr attribute" return obj.epics_attr diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index ea7637f4..ef5e8ba0 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -120,14 +120,36 @@ def attach_epics(queryset, as_field="epics_attr"): FROM "epics_relateduserstory" INNER JOIN "epics_epic" ON "epics_epic"."id" = "epics_relateduserstory"."epic_id" INNER JOIN "projects_project" ON "projects_project"."id" = "epics_epic"."project_id" - WHERE "epics_relateduserstory"."user_story_id" = {tbl}.id) t""" + WHERE "epics_relateduserstory"."user_story_id" = {tbl}.id + ORDER BY "projects_project"."name", "epics_epic"."ref") t""" sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset -def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False): +def attach_epic_order(queryset, epic_id, as_field="epic_order"): + """Attach epic_order column to each object of the queryset. + + :param queryset: A Django user stories queryset object. + :param epic_id: Order related to this epic. + :param as_field: Attach order as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + + model = queryset.model + sql = """SELECT "epics_relateduserstory"."order" AS "epic_order" + FROM "epics_relateduserstory" + WHERE "epics_relateduserstory"."user_story_id" = {tbl}.id and + "epics_relateduserstory"."epic_id" = {epic_id}""" + + sql = sql.format(tbl=model._meta.db_table, epic_id=epic_id) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False, epic_id=None): queryset = attach_total_points(queryset) queryset = attach_role_points(queryset) queryset = attach_epics(queryset) @@ -140,6 +162,10 @@ def attach_extra_info(queryset, user=None, include_attachments=False, include_ta queryset = attach_tasks(queryset) queryset = queryset.extra(select={"include_tasks": "True"}) + if epic_id is not None: + queryset = attach_epic_order(queryset, epic_id) + queryset = queryset.extra(select={"include_epic_order": "True"}) + queryset = attach_total_voters_to_queryset(queryset) queryset = attach_watchers_to_queryset(queryset) queryset = attach_total_watchers_to_queryset(queryset) From 15aa7da858cf433ce4add5ae067b46acd10bfaca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 28 Jul 2016 12:23:44 +0200 Subject: [PATCH 188/261] Apply some fixes over epic custom attributes --- .../migrations/0008_auto_20160728_0540.py | 95 ++++++++++--------- ...630_0849.py => 0009_auto_20160728_1002.py} | 26 ++--- taiga/projects/custom_attributes/models.py | 1 + 3 files changed, 65 insertions(+), 57 deletions(-) rename taiga/projects/custom_attributes/migrations/{0008_auto_20160630_0849.py => 0009_auto_20160728_1002.py} (88%) diff --git a/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py b/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py index 6f2d86f7..4c0509bb 100644 --- a/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py +++ b/taiga/projects/custom_attributes/migrations/0008_auto_20160728_0540.py @@ -15,50 +15,50 @@ class Migration(migrations.Migration): # Function: Remove a key in a json field migrations.RunSQL( """ - CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) - RETURNS json - LANGUAGE sql - IMMUTABLE - STRICT - AS $function$ - SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}') - FROM json_each("json") - WHERE "key" <> ALL ("keys_to_delete")), - '{}')::json $function$; + CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) + RETURNS json + LANGUAGE sql + IMMUTABLE + STRICT + AS $function$ + SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}') + FROM json_each("json") + WHERE "key" <> ALL ("keys_to_delete")), + '{}')::json $function$; """, - reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) - CASCADE;""" + reverse_sql=""" + DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[]) + CASCADE;""" ), # Function: Romeve a key in the json field of *_custom_attributes_values.values migrations.RunSQL( """ - CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"() - RETURNS trigger - AS $clean_key_in_custom_attributes_values$ - DECLARE - key text; - project_id int; - object_id int; - attribute text; - tablename text; - custom_attributes_tablename text; - BEGIN - key := OLD.id::text; - project_id := OLD.project_id; - attribute := TG_ARGV[0]::text; - tablename := TG_ARGV[1]::text; - custom_attributes_tablename := TG_ARGV[2]::text; - - EXECUTE 'UPDATE ' || quote_ident(custom_attributes_tablename) || ' - SET attributes_values = json_object_delete_keys(attributes_values, ' || quote_literal(key) || ') - FROM ' || quote_ident(tablename) || ' - WHERE ' || quote_ident(tablename) || '.project_id = ' || project_id || ' - AND ' || quote_ident(custom_attributes_tablename) || '.' || quote_ident(attribute) || ' = ' || quote_ident(tablename) || '.id'; - RETURN NULL; - END; $clean_key_in_custom_attributes_values$ - LANGUAGE plpgsql; + CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"() + RETURNS trigger + AS $clean_key_in_custom_attributes_values$ + DECLARE + key text; + project_id int; + object_id int; + attribute text; + tablename text; + custom_attributes_tablename text; + BEGIN + key := OLD.id::text; + project_id := OLD.project_id; + attribute := TG_ARGV[0]::text; + tablename := TG_ARGV[1]::text; + custom_attributes_tablename := TG_ARGV[2]::text; + EXECUTE 'UPDATE ' || quote_ident(custom_attributes_tablename) || ' + SET attributes_values = json_object_delete_keys(attributes_values, ' || quote_literal(key) || ') + FROM ' || quote_ident(tablename) || ' + WHERE ' || quote_ident(tablename) || '.project_id = ' || project_id || ' + AND ' || quote_ident(custom_attributes_tablename) || '.' || quote_ident(attribute) || ' = ' || quote_ident(tablename) || '.id'; + RETURN NULL; + END; $clean_key_in_custom_attributes_values$ + LANGUAGE plpgsql; """ ), @@ -66,13 +66,14 @@ class Migration(migrations.Migration): migrations.RunSQL( """ DROP TRIGGER IF EXISTS "update_userstorycustomvalues_after_remove_userstorycustomattribute" - ON custom_attributes_userstorycustomattribute - CASCADE; + ON custom_attributes_userstorycustomattribute + CASCADE; CREATE TRIGGER "update_userstorycustomvalues_after_remove_userstorycustomattribute" AFTER DELETE ON custom_attributes_userstorycustomattribute FOR EACH ROW - EXECUTE PROCEDURE clean_key_in_custom_attributes_values('user_story_id', 'userstories_userstory', 'custom_attributes_userstorycustomattributesvalues'); + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('user_story_id', 'userstories_userstory', + 'custom_attributes_userstorycustomattributesvalues'); """ ), @@ -80,13 +81,14 @@ class Migration(migrations.Migration): migrations.RunSQL( """ DROP TRIGGER IF EXISTS "update_taskcustomvalues_after_remove_taskcustomattribute" - ON custom_attributes_taskcustomattribute - CASCADE; + ON custom_attributes_taskcustomattribute + CASCADE; CREATE TRIGGER "update_taskcustomvalues_after_remove_taskcustomattribute" AFTER DELETE ON custom_attributes_taskcustomattribute FOR EACH ROW - EXECUTE PROCEDURE clean_key_in_custom_attributes_values('task_id', 'tasks_task', 'custom_attributes_taskcustomattributesvalues'); + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('task_id', 'tasks_task', + 'custom_attributes_taskcustomattributesvalues'); """ ), @@ -94,13 +96,14 @@ class Migration(migrations.Migration): migrations.RunSQL( """ DROP TRIGGER IF EXISTS "update_issuecustomvalues_after_remove_issuecustomattribute" - ON custom_attributes_issuecustomattribute - CASCADE; + ON custom_attributes_issuecustomattribute + CASCADE; CREATE TRIGGER "update_issuecustomvalues_after_remove_issuecustomattribute" AFTER DELETE ON custom_attributes_issuecustomattribute FOR EACH ROW - EXECUTE PROCEDURE clean_key_in_custom_attributes_values('issue_id', 'issues_issue', 'custom_attributes_issuecustomattributesvalues'); + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('issue_id', 'issues_issue', + 'custom_attributes_issuecustomattributesvalues'); """ ), migrations.AlterIndexTogether( diff --git a/taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py b/taiga/projects/custom_attributes/migrations/0009_auto_20160728_1002.py similarity index 88% rename from taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py rename to taiga/projects/custom_attributes/migrations/0009_auto_20160728_1002.py index bcb7668c..313e22fd 100644 --- a/taiga/projects/custom_attributes/migrations/0008_auto_20160630_0849.py +++ b/taiga/projects/custom_attributes/migrations/0009_auto_20160728_1002.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-06-30 08:49 +# Generated by Django 1.9.2 on 2016-07-28 10:02 from __future__ import unicode_literals from django.db import migrations, models @@ -11,12 +11,13 @@ import django_pgjson.fields class Migration(migrations.Migration): dependencies = [ - ('epics', '0001_initial'), - ('projects', '0049_auto_20160629_1443'), - ('custom_attributes', '0007_auto_20160208_1751'), + ('epics', '0002_epic_color'), + ('projects', '0050_project_epics_csv_uuid'), + ('custom_attributes', '0008_auto_20160728_0540'), ] operations = [ + # Change some verbose names migrations.AlterModelOptions( name='issuecustomattributesvalues', options={'ordering': ['id'], 'verbose_name': 'issue custom attributes values', 'verbose_name_plural': 'issue custom attributes values'}, @@ -29,7 +30,7 @@ class Migration(migrations.Migration): name='userstorycustomattributesvalues', options={'ordering': ['id'], 'verbose_name': 'user story custom attributes values', 'verbose_name_plural': 'user story custom attributes values'}, ), - + # Custom attributes for epics migrations.CreateModel( name='EpicCustomAttribute', fields=[ @@ -43,10 +44,10 @@ class Migration(migrations.Migration): ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='epiccustomattributes', to='projects.Project', verbose_name='project')), ], options={ - 'verbose_name_plural': 'epic custom attributes', 'verbose_name': 'epic custom attribute', 'abstract': False, 'ordering': ['project', 'order', 'name'], + 'verbose_name_plural': 'epic custom attributes', }, ), migrations.CreateModel( @@ -58,24 +59,27 @@ class Migration(migrations.Migration): ('epic', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='custom_attributes_values', to='epics.Epic', verbose_name='epic')), ], options={ - 'verbose_name_plural': 'epic custom attributes values', - 'verbose_name': 'epic custom attributes values', 'abstract': False, + 'verbose_name': 'epic custom attributes values', 'ordering': ['id'], + 'verbose_name_plural': 'epic custom attributes values', }, ), + migrations.AlterIndexTogether( + name='epiccustomattributesvalues', + index_together=set([('epic',)]), + ), migrations.AlterUniqueTogether( name='epiccustomattribute', unique_together=set([('project', 'name')]), ), - - # Trigger: Clean epiccustomattributes values before remove a epiccustomattribute migrations.RunSQL( """ CREATE TRIGGER "update_epiccustomvalues_after_remove_epiccustomattribute" AFTER DELETE ON custom_attributes_epiccustomattribute FOR EACH ROW - EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_epiccustomattributesvalues'); + EXECUTE PROCEDURE clean_key_in_custom_attributes_values('epic_id', 'epics_epic', + 'custom_attributes_epiccustomattributesvalues'); """, reverse_sql="""DROP TRIGGER IF EXISTS "update_epiccustomvalues_after_remove_epiccustomattribute" ON custom_attributes_epiccustomattribute diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 8b5747f0..4fa6978b 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -106,6 +106,7 @@ class EpicCustomAttributesValues(AbstractCustomAttributesValues): class Meta(AbstractCustomAttributesValues.Meta): verbose_name = "epic custom attributes values" verbose_name_plural = "epic custom attributes values" + index_together = [("epic",)] @property def project(self): From 3dbde420d8d68b2966c0fb2f529a9209d6dd56fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 29 Jul 2016 10:02:57 +0200 Subject: [PATCH 189/261] Add missed migrations --- .../migrations/0051_auto_20160729_0802.py | 19 +++++++++++++++++++ taiga/projects/models.py | 16 ++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 taiga/projects/migrations/0051_auto_20160729_0802.py diff --git a/taiga/projects/migrations/0051_auto_20160729_0802.py b/taiga/projects/migrations/0051_auto_20160729_0802.py new file mode 100644 index 00000000..24767fdb --- /dev/null +++ b/taiga/projects/migrations/0051_auto_20160729_0802.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-29 08:02 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0050_project_epics_csv_uuid'), + ] + + operations = [ + migrations.AlterModelOptions( + name='project', + options={'ordering': ['name', 'id'], 'verbose_name': 'project', 'verbose_name_plural': 'projects'}, + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 080cd411..1642426c 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -87,6 +87,12 @@ class Membership(models.Model): user_order = models.IntegerField(default=10000, null=False, blank=False, verbose_name=_("user order")) + class Meta: + verbose_name = "membership" + verbose_name_plural = "memberships" + unique_together = ("user", "project",) + ordering = ["project", "user__full_name", "user__username", "user__email", "email"] + def get_related_people(self): related_people = get_user_model().objects.filter(id=self.user.id) return related_people @@ -97,12 +103,6 @@ class Membership(models.Model): if self.user and memberships.count() > 0 and memberships[0].id != self.id: raise ValidationError(_('The user is already member of the project')) - class Meta: - verbose_name = "membership" - verbose_name_plural = "memberships" - unique_together = ("user", "project",) - ordering = ["project", "user__full_name", "user__username", "user__email", "email"] - class ProjectDefaults(models.Model): default_epic_status = models.OneToOneField("projects.EpicStatus", @@ -258,10 +258,6 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): ["name", "id"], ] - permissions = ( - ("view_project", "Can view project"), - ) - def __str__(self): return self.name From 133cf149fd73f9e69019bc4b4f8724cd66680864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 29 Jul 2016 12:39:51 +0200 Subject: [PATCH 190/261] Improve Epic sort feature --- taiga/projects/epics/api.py | 5 ++--- taiga/projects/epics/services.py | 26 ++++++++++---------------- tests/unit/test_order_updates.py | 2 -- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index bbfd164f..6f4b18e9 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -200,12 +200,11 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) - services.update_epics_order_in_bulk(data["bulk_epics"], + ret = services.update_epics_order_in_bulk(data["bulk_epics"], project=project, field=order_field) - services.snapshot_epics_in_bulk(data["bulk_epics"], request.user) - return response.NoContent() + return response.Ok(ret) @list_route(methods=["POST"]) def bulk_update_epics_order(self, request, **kwargs): diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index 8674e79f..ea470e8f 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -27,6 +27,7 @@ from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot +from taiga.projects.services import apply_order_updates from taiga.projects.epics.apps import connect_epics_signals from taiga.projects.epics.apps import disconnect_epics_signals from taiga.events import events @@ -78,28 +79,21 @@ def update_epics_order_in_bulk(bulk_data: list, field: str, project: object): Update the order of some epics. `bulk_data` should be a list of tuples with the following format: - [(, {: , ...}), ...] + [{'epic_id': , 'order': }, ...] """ - epic_ids = [] - new_order_values = [] - for epic_data in bulk_data: - epic_ids.append(epic_data["epic_id"]) - new_order_values.append({field: epic_data["order"]}) + epics = project.epics.all() + epic_orders = {e.id: getattr(e, field) for e in epics} + new_epic_orders = {d["epic_id"]: d["order"] for d in bulk_data} + apply_order_updates(epic_orders, new_epic_orders) + + epic_ids = epic_orders.keys() events.emit_event_for_ids(ids=epic_ids, content_type="epics.epic", projectid=project.pk) - db.update_in_bulk_with_ids(epic_ids, new_order_values, model=models.Epic) - - -def snapshot_epics_in_bulk(bulk_data, user): - for epic_data in bulk_data: - try: - epic = models.Epic.objects.get(pk=epic_data['epic_id']) - take_snapshot(epic, user=user) - except models.Epic.DoesNotExist: - pass + db.update_attr_in_bulk_for_ids(epic_orders, field, models.Epic) + return epic_orders ##################################################### diff --git a/tests/unit/test_order_updates.py b/tests/unit/test_order_updates.py index f7660bf0..1a4a571c 100644 --- a/tests/unit/test_order_updates.py +++ b/tests/unit/test_order_updates.py @@ -131,7 +131,6 @@ def test_apply_order_updates_duplicated_orders(): "a": 3 } apply_order_updates(orders, new_orders) - print(orders) assert orders == { "a": 3, "c": 4, @@ -155,7 +154,6 @@ def test_apply_order_updates_multiple_elements_duplicated_orders(): "a": 4 } apply_order_updates(orders, new_orders) - print(orders) assert orders == { "c": 3, "d": 3, From 783dca147265c57e6d0cdd8dec9a7c1e2f7ca610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 29 Jul 2016 18:56:33 +0200 Subject: [PATCH 191/261] Minor fixes --- taiga/projects/epics/models.py | 12 ++++++------ taiga/projects/milestones/utils.py | 4 ++++ taiga/projects/userstories/services.py | 4 ++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index 6b7d9231..eb18c6c4 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -76,6 +76,12 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M verbose_name_plural = "epics" ordering = ["project", "epics_order", "ref"] + def __str__(self): + return "#{0} {1}".format(self.ref, self.subject) + + def __repr__(self): + return "" % (self.id) + def save(self, *args, **kwargs): if not self._importing or not self.modified_date: self.modified_date = timezone.now() @@ -85,12 +91,6 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M super().save(*args, **kwargs) - def __str__(self): - return "#{0} {1}".format(self.ref, self.subject) - - def __repr__(self): - return "" % (self.id) - class RelatedUserStory(models.Model): user_story = models.ForeignKey("userstories.UserStory", on_delete=models.CASCADE) diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py index b292b1bd..bea1cf12 100644 --- a/taiga/projects/milestones/utils.py +++ b/taiga/projects/milestones/utils.py @@ -80,6 +80,8 @@ def attach_extra_info(queryset, user=None): us_queryset = userstories_utils.attach_total_points(us_queryset) us_queryset = userstories_utils.attach_role_points(us_queryset) + us_queryset = userstories_utils.attach_epics(us_queryset) + us_queryset = attach_total_voters_to_queryset(us_queryset) us_queryset = attach_watchers_to_queryset(us_queryset) us_queryset = attach_total_watchers_to_queryset(us_queryset) @@ -87,6 +89,7 @@ def attach_extra_info(queryset, user=None): us_queryset = attach_is_watcher_to_queryset(us_queryset, user) queryset = queryset.prefetch_related(Prefetch("user_stories", queryset=us_queryset)) + queryset = attach_total_points(queryset) queryset = attach_closed_points(queryset) @@ -95,4 +98,5 @@ def attach_extra_info(queryset, user=None): queryset = attach_total_watchers_to_queryset(queryset) queryset = attach_is_voter_to_queryset(queryset, user) queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 11fb2a2f..5998a57e 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -469,6 +469,8 @@ def _get_userstories_epics(project, queryset): FROM "epics_relateduserstory" INNER JOIN "userstories_userstory" ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") WHERE {where} GROUP BY "epics_relateduserstory"."epic_id" ) @@ -492,6 +494,8 @@ def _get_userstories_epics(project, queryset): FROM "userstories_userstory" LEFT OUTER JOIN "epics_relateduserstory" ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL GROUP BY "epics_relateduserstory"."epic_id" """.format(where=where) From 0ff7ce8975bfdbd66a83623baf020a0f4142bb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 1 Aug 2016 13:10:15 +0200 Subject: [PATCH 192/261] Add is_closed to StatusEstraInfoSerializer --- taiga/projects/mixins/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index cec50b2b..67949877 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -73,7 +73,8 @@ class StatusExtraInfoSerializerMixin(serializers.LightSerializer): if serialized_status is None: serialized_status = { "name": _(obj.status.name), - "color": obj.status.color + "color": obj.status.color, + "is_closed": obj.status.is_closed } self._serialized_status[obj.status_id] = serialized_status From 9d0e0180efa9a2ca68a7b890b7069c1c7ec1c318 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 1 Aug 2016 15:10:11 +0200 Subject: [PATCH 193/261] Import export including epics --- taiga/export_import/serializers/cache.py | 2 +- .../export_import/serializers/serializers.py | 63 ++++++++ taiga/export_import/services/render.py | 8 +- taiga/export_import/services/store.py | 140 +++++++++++++++--- taiga/export_import/validators/__init__.py | 4 + taiga/export_import/validators/cache.py | 1 + taiga/export_import/validators/validators.py | 53 +++++++ tests/unit/test_export.py | 14 ++ tests/unit/test_import.py | 57 +++++++ 9 files changed, 319 insertions(+), 23 deletions(-) create mode 100644 tests/unit/test_import.py diff --git a/taiga/export_import/serializers/cache.py b/taiga/export_import/serializers/cache.py index c4eb5bfa..f22978f8 100644 --- a/taiga/export_import/serializers/cache.py +++ b/taiga/export_import/serializers/cache.py @@ -23,7 +23,7 @@ _cache_user_by_email = {} _custom_tasks_attributes_cache = {} _custom_issues_attributes_cache = {} _custom_userstories_attributes_cache = {} - +_custom_epics_attributes_cache = {} def cached_get_user_by_pk(pk): if pk not in _cache_user_by_pk: diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py index ff7e791c..f4f46e52 100644 --- a/taiga/export_import/serializers/serializers.py +++ b/taiga/export_import/serializers/serializers.py @@ -29,6 +29,7 @@ from .mixins import (HistoryExportSerializerMixin, WatcheableObjectLightSerializerMixin) from .cache import (_custom_tasks_attributes_cache, _custom_userstories_attributes_cache, + _custom_epics_attributes_cache, _custom_issues_attributes_cache) @@ -55,6 +56,14 @@ class UserStoryStatusExportSerializer(RelatedExportSerializer): wip_limit = Field() +class EpicStatusExportSerializer(RelatedExportSerializer): + name = Field() + slug = Field() + order = Field() + is_closed = Field() + color = Field() + + class TaskStatusExportSerializer(RelatedExportSerializer): name = Field() slug = Field() @@ -97,6 +106,15 @@ class RoleExportSerializer(RelatedExportSerializer): permissions = Field() +class EpicCustomAttributesExportSerializer(RelatedExportSerializer): + name = Field() + description = Field() + type = Field() + order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() + + class UserStoryCustomAttributeExportSerializer(RelatedExportSerializer): name = Field() description = Field() @@ -238,6 +256,45 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, return _custom_userstories_attributes_cache[project.id] +class EpicRelatedUserStoryExportSerializer(RelatedExportSerializer): + user_story = SlugRelatedField(slug_field="ref") + order = Field() + + +class EpicExportSerializer(CustomAttributesValuesExportSerializerMixin, + HistoryExportSerializerMixin, + AttachmentExportSerializerMixin, + WatcheableObjectLightSerializerMixin, + RelatedExportSerializer): + ref = Field() + owner = UserRelatedField() + status = SlugRelatedField(slug_field="name") + epics_order = Field() + created_date = DateTimeField() + modified_date = DateTimeField() + subject = Field() + description = Field() + color = Field() + assigned_to = UserRelatedField() + client_requirement = Field() + team_requirement = Field() + version = Field() + blocked_note = Field() + is_blocked = Field() + tags = Field() + related_user_stories = MethodField() + + def get_related_user_stories(self, obj): + return EpicRelatedUserStoryExportSerializer(obj.relateduserstory_set.all(), many=True).data + + def custom_attributes_queryset(self, project): + if project.id not in _custom_epics_attributes_cache: + _custom_epics_attributes_cache[project.id] = list( + project.userstorycustomattributes.all().values('id', 'name') + ) + return _custom_epics_attributes_cache[project.id] + + class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin, AttachmentExportSerializerMixin, @@ -307,6 +364,7 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin): logo = FileField() total_milestones = Field() total_story_points = Field() + is_epics_activated = Field() is_backlog_activated = Field() is_kanban_activated = Field() is_wiki_activated = Field() @@ -318,6 +376,7 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin): is_featured = Field() is_looking_for_people = Field() looking_for_people_note = Field() + epics_csv_uuid = Field() userstories_csv_uuid = Field() tasks_csv_uuid = Field() issues_csv_uuid = Field() @@ -339,6 +398,7 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin): owner = UserRelatedField() memberships = MembershipExportSerializer(many=True) points = PointsExportSerializer(many=True) + epic_statuses = EpicStatusExportSerializer(many=True) us_statuses = UserStoryStatusExportSerializer(many=True) task_statuses = TaskStatusExportSerializer(many=True) issue_types = IssueTypeExportSerializer(many=True) @@ -347,15 +407,18 @@ class ProjectExportSerializer(WatcheableObjectLightSerializerMixin): severities = SeverityExportSerializer(many=True) tags_colors = Field() default_points = SlugRelatedField(slug_field="name") + default_epic_status = SlugRelatedField(slug_field="name") default_us_status = SlugRelatedField(slug_field="name") default_task_status = SlugRelatedField(slug_field="name") default_priority = SlugRelatedField(slug_field="name") default_severity = SlugRelatedField(slug_field="name") default_issue_status = SlugRelatedField(slug_field="name") default_issue_type = SlugRelatedField(slug_field="name") + epiccustomattributes = EpicCustomAttributesExportSerializer(many=True) userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True) taskcustomattributes = TaskCustomAttributeExportSerializer(many=True) issuecustomattributes = IssueCustomAttributeExportSerializer(many=True) + epics = EpicExportSerializer(many=True) user_stories = UserStoryExportSerializer(many=True) tasks = TaskExportSerializer(many=True) milestones = MilestoneExportSerializer(many=True) diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py index 0b56f3f5..cb757dd0 100644 --- a/taiga/export_import/services/render.py +++ b/taiga/export_import/services/render.py @@ -45,12 +45,16 @@ def render_project(project, outfile, chunk_size=8190): # field.initialize(parent=serializer, field_name=field_name) # These four "special" fields hava attachments so we use them in a special way - if field_name in ["wiki_pages", "user_stories", "tasks", "issues"]: + if field_name in ["wiki_pages", "user_stories", "tasks", "issues", "epics"]: value = get_component(project, field_name) if field_name != "wiki_pages": - value = value.select_related('owner', 'status', 'milestone', + value = value.select_related('owner', 'status', 'project', 'assigned_to', 'custom_attributes_values') + + if field_name in ["user_stories", "tasks", "issues"]: + value = value.select_related('milestone') + if field_name == "issues": value = value.select_related('severity', 'priority', 'type') value = value.prefetch_related('history_entry', 'attachments') diff --git a/taiga/export_import/services/store.py b/taiga/export_import/services/store.py index 9739bb1e..e28353bc 100644 --- a/taiga/export_import/services/store.py +++ b/taiga/export_import/services/store.py @@ -80,11 +80,17 @@ def store_project(data): excluded_fields = [ "default_points", "default_us_status", "default_task_status", "default_priority", "default_severity", "default_issue_status", - "default_issue_type", "memberships", "points", "us_statuses", - "task_statuses", "issue_statuses", "priorities", "severities", - "issue_types", "userstorycustomattributes", "taskcustomattributes", - "issuecustomattributes", "roles", "milestones", "wiki_pages", - "wiki_links", "notify_policies", "user_stories", "issues", "tasks", + "default_issue_type", "default_epic_status", + "memberships", "points", + "epic_statuses", "us_statuses", "task_statuses", "issue_statuses", + "priorities", "severities", + "issue_types", + "epiccustomattributes", "userstorycustomattributes", + "taskcustomattributes", "issuecustomattributes", + "roles", "milestones", + "wiki_pages", "wiki_links", + "notify_policies", + "epics", "user_stories", "issues", "tasks", "is_featured" ] if key not in excluded_fields: @@ -195,7 +201,7 @@ def _store_membership(project, membership): validator.object._importing = True validator.object.token = str(uuid.uuid1()) validator.object.user = find_invited_user(validator.object.email, - default=validator.object.user) + default=validator.object.user) validator.save() return validator @@ -219,6 +225,7 @@ def _store_project_attribute_value(project, data, field, serializer): validator.object._importing = True validator.save() return validator.object + add_errors(field, validator.errors) return None @@ -239,10 +246,10 @@ def store_default_project_attributes_values(project, data): else: value = related.all().first() setattr(project, field, value) - helper(project, "default_points", project.points, data) helper(project, "default_issue_type", project.issue_types, data) helper(project, "default_issue_status", project.issue_statuses, data) + helper(project, "default_epic_status", project.epic_statuses, data) helper(project, "default_us_status", project.us_statuses, data) helper(project, "default_task_status", project.task_statuses, data) helper(project, "default_priority", project.priorities, data) @@ -317,12 +324,14 @@ def _store_role_point(project, us, role_point): add_errors("role_points", validator.errors) return None + def store_user_story(project, data): if "status" not in data and project.default_us_status: data["status"] = project.default_us_status.name us_data = {key: value for key, value in data.items() if key not in - ["role_points", "custom_attributes_values"]} + ["role_points", "custom_attributes_values"]} + validator = validators.UserStoryExportValidator(data=us_data, context={"project": project}) if validator.is_valid(): @@ -360,10 +369,13 @@ def store_user_story(project, data): custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: custom_attributes = validator.object.project.userstorycustomattributes.all().values('id', 'name') - custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values( - custom_attributes, custom_attributes_values) + custom_attributes_values = \ + _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) + _store_custom_attributes_values(validator.object, custom_attributes_values, - "user_story", validators.UserStoryCustomAttributesValuesExportValidator) + "user_story", + validators.UserStoryCustomAttributesValuesExportValidator) return validator @@ -379,6 +391,81 @@ def store_user_stories(project, data): return results +## EPICS + +def _store_epic_related_user_story(project, epic, related_user_story): + validator = validators.EpicRelatedUserStoryExportValidator(data=related_user_story, + context={"project": project}) + if validator.is_valid(): + validator.object.epic = epic + validator.object.save() + return validator.object + + add_errors("epic_related_user_stories", validator.errors) + return None + + +def store_epic(project, data): + if "status" not in data and project.default_epic_status: + data["status"] = project.default_epic_status.name + + validator = validators.EpicExportValidator(data=data, context={"project": project}) + if validator.is_valid(): + validator.object.project = project + if validator.object.owner is None: + validator.object.owner = validator.object.project.owner + validator.object._importing = True + validator.object._not_notify = True + + validator.save() + validator.save_watchers() + + if validator.object.ref: + sequence_name = refs.make_sequence_name(project) + if not seq.exists(sequence_name): + seq.create(sequence_name) + seq.set_max(sequence_name, validator.object.ref) + else: + validator.object.ref, _ = refs.make_reference(validator.object, project) + validator.object.save() + + for epic_attachment in data.get("attachments", []): + _store_attachment(project, validator.object, epic_attachment) + + for related_user_story in data.get("related_user_stories", []): + _store_epic_related_user_story(project, validator.object, related_user_story) + + history_entries = data.get("history", []) + for history in history_entries: + _store_history(project, validator.object, history) + + if not history_entries: + take_snapshot(validator.object, user=validator.object.owner) + + custom_attributes_values = data.get("custom_attributes_values", None) + if custom_attributes_values: + custom_attributes = validator.object.project.epiccustomattributes.all().values('id', 'name') + custom_attributes_values = \ + _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) + _store_custom_attributes_values(validator.object, custom_attributes_values, + "epic", + validators.EpicCustomAttributesValuesExportValidator) + + return validator + + add_errors("epics", validator.errors) + return None + + +def store_epics(project, data): + results = [] + for epic in data.get("epics", []): + epic = store_epic(project, epic) + results.append(epic) + return results + + ## TASKS def store_task(project, data): @@ -418,10 +505,13 @@ def store_task(project, data): custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: custom_attributes = validator.object.project.taskcustomattributes.all().values('id', 'name') - custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values( - custom_attributes, custom_attributes_values) + custom_attributes_values = \ + _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) + _store_custom_attributes_values(validator.object, custom_attributes_values, - "task", validators.TaskCustomAttributesValuesExportValidator) + "task", + validators.TaskCustomAttributesValuesExportValidator) return validator @@ -486,10 +576,12 @@ def store_issue(project, data): custom_attributes_values = data.get("custom_attributes_values", None) if custom_attributes_values: custom_attributes = validator.object.project.issuecustomattributes.all().values('id', 'name') - custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values( - custom_attributes, custom_attributes_values) + custom_attributes_values = \ + _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, + custom_attributes_values) _store_custom_attributes_values(validator.object, custom_attributes_values, - "issue", validators.IssueCustomAttributesValuesExportValidator) + "issue", + validators.IssueCustomAttributesValuesExportValidator) return validator @@ -605,8 +697,9 @@ def _validate_if_owner_have_enought_space_to_this_project(owner, data): is_private = data.get("is_private", False) total_memberships = len([m for m in data.get("memberships", []) - if m.get("email", None) != data["owner"]]) - total_memberships = total_memberships + 1 # 1 is the owner + if m.get("email", None) != data["owner"]]) + + total_memberships = total_memberships + 1 # 1 is the owner (enough_slots, error_message) = users_service.has_available_slot_for_import_new_project( owner, is_private, @@ -652,9 +745,10 @@ def _populate_project_object(project, data): # Create memberships store_memberships(project, data) _create_membership_for_project_owner(project) - check_if_there_is_some_error(_("error importing memberships"), project) + check_if_there_is_some_error(_("error importing memberships"), project) # Create project attributes values + store_project_attributes_values(project, data, "epic_statuses", validators.EpicStatusExportValidator) store_project_attributes_values(project, data, "us_statuses", validators.UserStoryStatusExportValidator) store_project_attributes_values(project, data, "points", validators.PointsExportValidator) store_project_attributes_values(project, data, "task_statuses", validators.TaskStatusExportValidator) @@ -669,6 +763,8 @@ def _populate_project_object(project, data): check_if_there_is_some_error(_("error importing default project attributes values"), project) # Create custom attributes + store_custom_attributes(project, data, "epiccustomattributes", + validators.EpicCustomAttributeExportValidator) store_custom_attributes(project, data, "userstorycustomattributes", validators.UserStoryCustomAttributeExportValidator) store_custom_attributes(project, data, "taskcustomattributes", @@ -689,6 +785,10 @@ def _populate_project_object(project, data): store_user_stories(project, data) check_if_there_is_some_error(_("error importing user stories"), project) + # Creat epics + store_epics(project, data) + check_if_there_is_some_error(_("error importing epics"), project) + # Createer tasks store_tasks(project, data) check_if_there_is_some_error(_("error importing tasks"), project) diff --git a/taiga/export_import/validators/__init__.py b/taiga/export_import/validators/__init__.py index 969a8d0c..0948ade0 100644 --- a/taiga/export_import/validators/__init__.py +++ b/taiga/export_import/validators/__init__.py @@ -1,4 +1,5 @@ from .validators import PointsExportValidator +from .validators import EpicStatusExportValidator from .validators import UserStoryStatusExportValidator from .validators import TaskStatusExportValidator from .validators import IssueStatusExportValidator @@ -6,6 +7,7 @@ from .validators import PriorityExportValidator from .validators import SeverityExportValidator from .validators import IssueTypeExportValidator from .validators import RoleExportValidator +from .validators import EpicCustomAttributeExportValidator from .validators import UserStoryCustomAttributeExportValidator from .validators import TaskCustomAttributeExportValidator from .validators import IssueCustomAttributeExportValidator @@ -17,6 +19,8 @@ from .validators import MembershipExportValidator from .validators import RolePointsExportValidator from .validators import MilestoneExportValidator from .validators import TaskExportValidator +from .validators import EpicRelatedUserStoryExportValidator +from .validators import EpicExportValidator from .validators import UserStoryExportValidator from .validators import IssueExportValidator from .validators import WikiPageExportValidator diff --git a/taiga/export_import/validators/cache.py b/taiga/export_import/validators/cache.py index c4eb5bfa..d82e943d 100644 --- a/taiga/export_import/validators/cache.py +++ b/taiga/export_import/validators/cache.py @@ -22,6 +22,7 @@ _cache_user_by_pk = {} _cache_user_by_email = {} _custom_tasks_attributes_cache = {} _custom_issues_attributes_cache = {} +_custom_epics_attributes_cache = {} _custom_userstories_attributes_cache = {} diff --git a/taiga/export_import/validators/validators.py b/taiga/export_import/validators/validators.py index 818df0c3..c821b531 100644 --- a/taiga/export_import/validators/validators.py +++ b/taiga/export_import/validators/validators.py @@ -25,6 +25,7 @@ from taiga.base.exceptions import ValidationError from taiga.projects import models as projects_models from taiga.projects.custom_attributes import models as custom_attributes_models +from taiga.projects.epics import models as epics_models from taiga.projects.userstories import models as userstories_models from taiga.projects.tasks import models as tasks_models from taiga.projects.issues import models as issues_models @@ -38,6 +39,7 @@ from .fields import (FileField, UserRelatedField, TimelineDataField, ContentTypeField) from .mixins import WatcheableObjectModelValidatorMixin from .cache import (_custom_tasks_attributes_cache, + _custom_epics_attributes_cache, _custom_userstories_attributes_cache, _custom_issues_attributes_cache) @@ -48,6 +50,12 @@ class PointsExportValidator(validators.ModelValidator): exclude = ('id', 'project') +class EpicStatusExportValidator(validators.ModelValidator): + class Meta: + model = projects_models.EpicStatus + exclude = ('id', 'project') + + class UserStoryStatusExportValidator(validators.ModelValidator): class Meta: model = projects_models.UserStoryStatus @@ -92,6 +100,14 @@ class RoleExportValidator(validators.ModelValidator): exclude = ('id', 'project') +class EpicCustomAttributeExportValidator(validators.ModelValidator): + modified_date = serializers.DateTimeField(required=False) + + class Meta: + model = custom_attributes_models.EpicCustomAttribute + exclude = ('id', 'project') + + class UserStoryCustomAttributeExportValidator(validators.ModelValidator): modified_date = serializers.DateTimeField(required=False) @@ -151,6 +167,15 @@ class BaseCustomAttributesValuesExportValidator(validators.ModelValidator): return attrs +class EpicCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): + _custom_attribute_model = custom_attributes_models.EpicCustomAttribute + _container_model = "epics.Epic" + _container_field = "epic" + + class Meta(BaseCustomAttributesValuesExportValidator.Meta): + model = custom_attributes_models.EpicCustomAttributesValues + + class UserStoryCustomAttributesValuesExportValidator(BaseCustomAttributesValuesExportValidator): _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute _container_model = "userstories.UserStory" @@ -244,6 +269,34 @@ class TaskExportValidator(WatcheableObjectModelValidatorMixin): return _custom_tasks_attributes_cache[project.id] +class EpicRelatedUserStoryExportValidator(validators.ModelValidator): + user_story = ProjectRelatedField(slug_field="ref") + order = serializers.IntegerField() + + class Meta: + model = epics_models.RelatedUserStory + exclude = ('id', 'epic') + + +class EpicExportValidator(WatcheableObjectModelValidatorMixin): + owner = UserRelatedField(required=False) + assigned_to = UserRelatedField(required=False) + status = ProjectRelatedField(slug_field="name") + modified_date = serializers.DateTimeField(required=False) + user_stories = EpicRelatedUserStoryExportValidator(many=True, required=False) + + class Meta: + model = epics_models.Epic + exclude = ('id', 'project') + + def custom_attributes_queryset(self, project): + if project.id not in _custom_epics_attributes_cache: + _custom_epics_attributes_cache[project.id] = list( + project.epiccustomattributes.all().values('id', 'name') + ) + return _custom_epics_attributes_cache[project.id] + + class UserStoryExportValidator(WatcheableObjectModelValidatorMixin): role_points = RolePointsExportValidator(many=True, required=False) owner = UserRelatedField(required=False) diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py index a8ce775f..1088550f 100644 --- a/tests/unit/test_export.py +++ b/tests/unit/test_export.py @@ -42,3 +42,17 @@ def test_export_user_story_finish_date(client): project_data = json.loads(output.getvalue()) finish_date = project_data["user_stories"][0]["finish_date"] assert finish_date == "2014-10-22T00:00:00+0000" + + +def test_export_epic_with_user_stories(client): + epic = f.EpicFactory.create(subject="test epic export") + user_story = f.UserStoryFactory.create(project=epic.project) + f.RelatedUserStory.create(epic=epic, user_story=user_story) + output = io.BytesIO() + render_project(user_story.project, output) + project_data = json.loads(output.getvalue()) + assert project_data["epics"][0]["subject"] == "test epic export" + assert len(project_data["epics"]) == 1 + + assert project_data["epics"][0]["related_user_stories"][0]["user_story"] == user_story.ref + assert len(project_data["epics"][0]["related_user_stories"]) == 1 diff --git a/tests/unit/test_import.py b/tests/unit/test_import.py new file mode 100644 index 00000000..58f9b9db --- /dev/null +++ b/tests/unit/test_import.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import pytest +import io +from .. import factories as f + +from taiga.base.utils import json +from taiga.export_import.services import render_project, store_project_from_dict + +pytestmark = pytest.mark.django_db + + +def test_import_epic_with_user_stories(client): + project = f.ProjectFactory() + project.default_points = f.PointsFactory.create(project=project) + project.default_issue_type = f.IssueTypeFactory.create(project=project) + project.default_issue_status = f.IssueStatusFactory.create(project=project) + project.default_epic_status = f.EpicStatusFactory.create(project=project) + project.default_us_status = f.UserStoryStatusFactory.create(project=project) + project.default_task_status = f.TaskStatusFactory.create(project=project) + project.default_priority = f.PriorityFactory.create(project=project) + project.default_severity = f.SeverityFactory.create(project=project) + + epic = f.EpicFactory.create(subject="test epic export", project=project, status=project.default_epic_status) + user_story = f.UserStoryFactory.create(project=project, status=project.default_us_status, milestone=None) + f.RelatedUserStory.create(epic=epic, user_story=user_story, order=55) + output = io.BytesIO() + render_project(user_story.project, output) + project_data = json.loads(output.getvalue()) + + epic.project.delete() + + project = store_project_from_dict(project_data) + assert project.epics.count() == 1 + assert project.epics.first().ref == epic.ref + + assert project.epics.first().user_stories.count() == 1 + related_userstory = project.epics.first().relateduserstory_set.first() + assert related_userstory.user_story.ref == user_story.ref + assert related_userstory.order == 55 + assert related_userstory.epic.ref == epic.ref From dd3b098d4e0f2ad002ed39172ac96229d857ea6f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 2 Aug 2016 10:33:37 +0200 Subject: [PATCH 194/261] Fixing _get_userstories_epics for projects without userstories --- taiga/projects/userstories/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 5998a57e..71377b52 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -517,7 +517,7 @@ def _get_userstories_epics(project, queryset): result = sorted(result, key=itemgetter("order")) # Add row when there is no user stories with no epics - if result[0]["id"] is not None: + if result == [] or result[0]["id"] is not None: result.insert(0, { "id": None, "ref": None, From 46f6fa71e6c99c83f7265965082c33ce495d1cfb Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 2 Aug 2016 12:49:33 +0200 Subject: [PATCH 195/261] Adding bulk_create_related_userstories endpoint to epics API --- taiga/base/utils/db.py | 2 ++ taiga/projects/epics/api.py | 29 ++++++++++++++-- taiga/projects/epics/permissions.py | 5 +-- taiga/projects/epics/services.py | 33 ++++++++++++++++++- taiga/projects/epics/validators.py | 5 +++ .../test_epics_resources.py | 28 ++++++++++++++++ tests/integration/test_epics.py | 18 ++++++++++ 7 files changed, 114 insertions(+), 6 deletions(-) diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py index 9769abee..a6c21d6b 100644 --- a/taiga/base/utils/db.py +++ b/taiga/base/utils/db.py @@ -83,6 +83,7 @@ def save_in_bulk(instances, callback=None, precall=None, **save_options): :params callback: Callback to call after each save. :params save_options: Additional options to use when saving each instance. """ + ret = [] if callback is None: callback = functions.noop @@ -98,6 +99,7 @@ def save_in_bulk(instances, callback=None, precall=None, **save_options): instance.save(**save_options) callback(instance, created=created) + return ret @transaction.atomic def update_in_bulk(instances, list_of_new_values, callback=None, precall=None): diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 6f4b18e9..5eba3406 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -22,7 +22,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api.utils import get_object_or_404 from taiga.base import filters, response from taiga.base import exceptions as exc -from taiga.base.decorators import list_route +from taiga.base.decorators import list_route, detail_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin @@ -201,8 +201,8 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, raise exc.Blocked(_("Blocked element")) ret = services.update_epics_order_in_bulk(data["bulk_epics"], - project=project, - field=order_field) + project=project, + field=order_field) return response.Ok(ret) @@ -210,6 +210,29 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, def bulk_update_epics_order(self, request, **kwargs): return self._bulk_update_order("epics_order", request, **kwargs) + @detail_route(methods=["POST"]) + def bulk_create_related_userstories(self, request, **kwargs): + validator = validators.CrateRelatedUserStoriesBulkValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data + obj = self.get_object() + project = obj.project + self.check_permissions(request, 'bulk_create_userstories', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + services.create_related_userstories_in_bulk( + data["userstories"], + obj, + project=project, + owner=request.user + ) + obj = self.get_queryset().get(id=obj.id) + epic_serialized = self.get_serializer_class()(obj) + return response.Ok(epic_serialized.data) + + return response.BadRequest(validator.errors) + class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): permission_classes = (permissions.EpicVotersPermission,) diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py index 86f2626e..489aeae2 100644 --- a/taiga/projects/epics/permissions.py +++ b/taiga/projects/epics/permissions.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated, IsSuperUser -from taiga.permissions.permissions import HasProjectPerm, IsProjectAdmin +from taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated +from taiga.base.api.permissions import IsSuperUser, HasProjectPerm, IsProjectAdmin from taiga.permissions.permissions import CommentAndOrUpdatePerm @@ -35,6 +35,7 @@ class EpicPermission(TaigaResourcePermission): csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_epic') bulk_update_order_perms = HasProjectPerm('modify_epic') + bulk_create_userstories_perms = HasProjectPerm('modify_epic') & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') watch_perms = IsAuthenticated() & HasProjectPerm('view_epics') diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index ea470e8f..145e2339 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -26,10 +26,12 @@ from django.db import connection from django.utils.translation import ugettext as _ from taiga.base.utils import db, text -from taiga.projects.history.services import take_snapshot from taiga.projects.services import apply_order_updates from taiga.projects.epics.apps import connect_epics_signals from taiga.projects.epics.apps import disconnect_epics_signals +from taiga.projects.userstories.apps import connect_userstories_signals +from taiga.projects.userstories.apps import disconnect_userstories_signals +from taiga.projects.userstories.services import get_userstories_from_bulk from taiga.events import events from taiga.projects.votes.utils import attach_total_voters_to_queryset from taiga.projects.notifications.utils import attach_watchers_to_queryset @@ -96,6 +98,35 @@ def update_epics_order_in_bulk(bulk_data: list, field: str, project: object): return epic_orders +def create_related_userstories_in_bulk(bulk_data, epic, **additional_fields): + """Create user stories from `bulk_data`. + + :param epic: Element where all the user stories will be contained + :param bulk_data: List of user stories in bulk format. + :param additional_fields: Additional fields when instantiating each user story. + + :return: List of created `Task` instances. + """ + userstories = get_userstories_from_bulk(bulk_data, **additional_fields) + disconnect_userstories_signals() + + try: + db.save_in_bulk(userstories) + related_userstories = [] + for userstory in userstories: + related_userstories.append( + models.RelatedUserStory( + user_story=userstory, + epic=epic + ) + ) + db.save_in_bulk(related_userstories) + finally: + connect_userstories_signals() + + return userstories + + ##################################################### # CSV ##################################################### diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 9d7a617f..6652928c 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -55,6 +55,11 @@ class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator, bulk_epics = serializers.CharField() +class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator, + validators.Validator): + userstories = serializers.CharField() + + # Order bulk validators class _EpicOrderBulkValidator(EpicExistsValidator, validators.Validator): diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index f4e33813..1d128485 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -664,6 +664,34 @@ def test_epic_action_bulk_create(client, data): assert results == [401, 403, 403, 451, 451] +def test_bulk_create_related_userstories(client, data): + public_url = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.public_epic.pk}) + private_url1 = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.private_epic1.pk}) + private_url2 = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.private_epic2.pk}) + blocked_url = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.blocked_epic.pk}) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({ + "userstories": "test1\ntest2", + }) + + results = helper_test_http_method(client, 'post', public_url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, bulk_data, users) + assert results == [404, 404, 404, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, bulk_data, users) + assert results == [404, 404, 404, 451, 451] + + def test_epic_action_upvote(client, data): public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 5f22f62f..d8e09999 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -66,3 +66,21 @@ def test_custom_fields_csv_generation(): assert row[17] == attr.name row = next(reader) assert row[17] == "val1" + + +def test_bulk_create_related_userstories(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + epic = f.EpicFactory.create(project=project) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + url = reverse('epics-bulk-create-related-userstories', kwargs={"pk": epic.pk}) + + data = { + "userstories": "test1\ntest2" + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + assert response.data['user_stories_counts'] == {'opened': 2, 'closed': 0} From 4d4f8a44a12f11addf690d2fc60fe62d8f9ea5c1 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 2 Aug 2016 13:13:33 +0200 Subject: [PATCH 196/261] Removing the bulk update order for epics and doing that processing on the update --- taiga/projects/epics/api.py | 60 ++++++++++++++++++----------- taiga/projects/epics/permissions.py | 1 - taiga/projects/epics/validators.py | 12 ------ tests/integration/test_epics.py | 24 +++++++++++- 4 files changed, 61 insertions(+), 36 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 5eba3406..f76d8c81 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -25,6 +25,7 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route, detail_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin +from taiga.base.utils import json from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, EpicStatus @@ -89,11 +90,48 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, if obj.status and obj.status.project != obj.project: raise exc.WrongArguments(_("You don't have permissions to set this status to this epic.")) + """ + Updating the epic order attribute can affect the ordering of another epics + This method generate a key for the epic and can be used to be compared before and after + saving + If there is any difference it means an extra ordering update must be done + """ + def _epics_order_key(self, obj): + return "{}-{}".format(obj.project_id, obj.epics_order) + def pre_save(self, obj): if not obj.id: obj.owner = self.request.user + else: + self._old_epics_order_key = self._epics_order_key(self.get_object()) + super().pre_save(obj) + def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr, project): + # Executes the extra ordering if there is a difference in the ordering keys + if old_order_key != order_key: + extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) + data = [{"epic_id": obj.id, "order": getattr(obj, order_attr)}] + for id, order in extra_orders.items(): + data.append({"epic_id": int(id), "order": order}) + + return services.update_epics_order_in_bulk(data, + field=order_attr, + project=project) + return {} + + def post_save(self, obj, created=False): + if not created: + # Let's reorder the related stuff after edit the element + orders_updated = self._reorder_if_needed(obj, + self._old_epics_order_key, + self._epics_order_key(obj), + "epics_order", + obj.project) + self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated) + + super().post_save(obj, created) + def update(self, request, *args, **kwargs): self.object = self.get_object_or_none() project_id = request.DATA.get('project', None) @@ -188,28 +226,6 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.BadRequest(validator.errors) - def _bulk_update_order(self, order_field, request, **kwargs): - validator = validators.UpdateEpicsOrderBulkValidator(data=request.DATA) - if not validator.is_valid(): - return response.BadRequest(validator.errors) - - data = validator.data - project = get_object_or_404(Project, pk=data["project_id"]) - - self.check_permissions(request, "bulk_update_order", project) - if project.blocked_code is not None: - raise exc.Blocked(_("Blocked element")) - - ret = services.update_epics_order_in_bulk(data["bulk_epics"], - project=project, - field=order_field) - - return response.Ok(ret) - - @list_route(methods=["POST"]) - def bulk_update_epics_order(self, request, **kwargs): - return self._bulk_update_order("epics_order", request, **kwargs) - @detail_route(methods=["POST"]) def bulk_create_related_userstories(self, request, **kwargs): validator = validators.CrateRelatedUserStoriesBulkValidator(data=request.DATA) diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py index 489aeae2..b009cfc2 100644 --- a/taiga/projects/epics/permissions.py +++ b/taiga/projects/epics/permissions.py @@ -34,7 +34,6 @@ class EpicPermission(TaigaResourcePermission): filters_data_perms = AllowAny() csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_epic') - bulk_update_order_perms = HasProjectPerm('modify_epic') bulk_create_userstories_perms = HasProjectPerm('modify_epic') & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 6652928c..450fefd6 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -58,15 +58,3 @@ class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator, class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator, validators.Validator): userstories = serializers.CharField() - - -# Order bulk validators - -class _EpicOrderBulkValidator(EpicExistsValidator, validators.Validator): - epic_id = serializers.IntegerField() - order = serializers.IntegerField() - - -class UpdateEpicsOrderBulkValidator(ProjectExistsValidator, validators.Validator): - project_id = serializers.IntegerField() - bulk_epics = _EpicOrderBulkValidator(many=True) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index d8e09999..b766fbf1 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -68,6 +68,29 @@ def test_custom_fields_csv_generation(): assert row[17] == "val1" +def test_update_epic_order(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + epic_1 = f.EpicFactory.create(project=project, epics_order=1, status=project.default_us_status) + epic_2 = f.EpicFactory.create(project=project, epics_order=2, status=project.default_us_status) + epic_3 = f.EpicFactory.create(project=project, epics_order=3, status=project.default_us_status) + f.MembershipFactory.create(project=project, user=user, is_admin=True) + + url = reverse('epics-detail', kwargs={"pk": epic_1.pk}) + data = { + "epics_order": 2, + "version": epic_1.version + } + + client.login(user) + response = client.json.patch(url, json.dumps(data)) + assert json.loads(response.get("taiga-info-order-updated")) == { + str(epic_1.id): 2, + str(epic_2.id): 3, + str(epic_3.id): 4 + } + + def test_bulk_create_related_userstories(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) @@ -81,6 +104,5 @@ def test_bulk_create_related_userstories(client): } client.login(user) response = client.json.post(url, json.dumps(data)) - print(response.data) assert response.status_code == 200 assert response.data['user_stories_counts'] == {'opened': 2, 'closed': 0} From 2f7384a76ba656113986eef4b0102d6a47afdb28 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 2 Aug 2016 15:20:49 +0200 Subject: [PATCH 197/261] Adding set_related_userstory end point to epics API --- taiga/projects/epics/api.py | 29 ++++++++++- taiga/projects/epics/permissions.py | 1 + taiga/projects/epics/validators.py | 7 ++- .../test_epics_resources.py | 48 +++++++++++++++++++ tests/integration/test_epics.py | 44 +++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index f76d8c81..ea78c86a 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -32,6 +32,7 @@ from taiga.projects.models import Project, EpicStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin +from taiga.projects.userstories.models import UserStory from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -208,7 +209,7 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, if validator.is_valid(): data = validator.data project = Project.objects.get(id=data["project_id"]) - self.check_permissions(request, 'bulk_create', project) + self.check_permissions(request, "bulk_create", project) if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) @@ -249,6 +250,32 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.BadRequest(validator.errors) + @detail_route(methods=["POST"]) + def set_related_userstory(self, request, **kwargs): + validator = validators.SetRelatedUserStoryValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data + epic = self.get_object() + project = epic.project + user_story = UserStory.objects.get(id=data["us_id"]) + self.check_permissions(request, "update", epic) + self.check_permissions(request, "select_related_userstory", user_story.project) + + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + obj, created = models.RelatedUserStory.objects.update_or_create( + epic=epic, + user_story=user_story, + defaults={ + "order": data["order"] + }) + epic = self.get_queryset().get(id=epic.id) + epic_serialized = self.get_serializer_class()(epic) + return response.Ok(epic_serialized.data) + + return response.BadRequest(validator.errors) + class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): permission_classes = (permissions.EpicVotersPermission,) diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py index b009cfc2..3cd31b07 100644 --- a/taiga/projects/epics/permissions.py +++ b/taiga/projects/epics/permissions.py @@ -35,6 +35,7 @@ class EpicPermission(TaigaResourcePermission): csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_epic') bulk_create_userstories_perms = HasProjectPerm('modify_epic') & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) + select_related_userstory_perms = HasProjectPerm('view_us') upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') watch_perms = IsAuthenticated() & HasProjectPerm('view_epics') diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 450fefd6..674d030f 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -22,10 +22,10 @@ from taiga.base.api import serializers from taiga.base.api import validators from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField -from taiga.projects.milestones.validators import MilestoneExistsValidator from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.userstories.validators import UserStoryExistsValidator from taiga.projects.validators import ProjectExistsValidator from . import models @@ -58,3 +58,8 @@ class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator, class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator, validators.Validator): userstories = serializers.CharField() + + +class SetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): + us_id = serializers.IntegerField() + order = serializers.IntegerField(required=False, default=10000) diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index 1d128485..7b7f87ee 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -153,6 +153,12 @@ def data(): status__project=m.blocked_project) m.blocked_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.blocked_epic.id) + + m.public_us = f.UserStoryFactory(project=m.public_project) + m.private_us1 = f.UserStoryFactory(project=m.private_project1) + m.private_us2 = f.UserStoryFactory(project=m.private_project2) + m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + m.public_project.default_epic_status = m.public_epic.status m.public_project.save() m.private_project1.default_epic_status = m.private_epic1.status @@ -692,6 +698,48 @@ def test_bulk_create_related_userstories(client, data): assert results == [404, 404, 404, 451, 451] +def test_set_related_user_story(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.public_epic.pk}) + edit_data = json.dumps({ + "us_id": data.public_us.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic1.pk}) + edit_data = json.dumps({ + "us_id": data.private_us1.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic2.pk}) + edit_data = json.dumps({ + "us_id": data.private_us2.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 200, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.blocked_epic.pk}) + edit_data = json.dumps({ + "us_id": data.blocked_us.pk, + "order": 33, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 451, 451] + + def test_epic_action_upvote(client, data): public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index b766fbf1..6f0df014 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -26,6 +26,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.epics import services +from taiga.projects.epics import models from .. import factories as f @@ -106,3 +107,46 @@ def test_bulk_create_related_userstories(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 200 assert response.data['user_stories_counts'] == {'opened': 2, 'closed': 0} + + +def test_set_related_userstory(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) + + url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) + + data = { + "us_id": us.id + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} + + +def test_set_related_userstory_existing(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) + + url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) + + data = { + "us_id": us.id, + "order": 77 + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} + + related_us = models.RelatedUserStory.objects.get(id=related_us.id) + assert related_us.order == 77 From 9bba8efde90478310ef1eba0df461fb87e635193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 2 Aug 2016 19:05:15 +0200 Subject: [PATCH 198/261] Fix tests --- taiga/projects/userstories/validators.py | 9 +++++++++ tests/integration/test_epics.py | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 60bedaf7..9ff6b2f3 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -36,6 +36,15 @@ from . import models import json +class UserStoryExistsValidator: + def validate_us_id(self, attrs, source): + value = attrs[source] + if not models.UserStory.objects.filter(pk=value).exists(): + msg = _("There's no user story with that id") + raise ValidationError(msg) + return attrs + + class RolePointsField(serializers.WritableField): def to_native(self, obj): return {str(o.role.id): o.points.id for o in obj.all()} diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 6f0df014..57a80d10 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -123,7 +123,6 @@ def test_set_related_userstory(client): } client.login(user) response = client.json.post(url, json.dumps(data)) - print(response.data) assert response.status_code == 200 assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} @@ -144,7 +143,6 @@ def test_set_related_userstory_existing(client): } client.login(user) response = client.json.post(url, json.dumps(data)) - print(response.data) assert response.status_code == 200 assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} From 0e6daf09f695d6717fb6f8fc1d2908625ea61f80 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 3 Aug 2016 08:40:33 +0200 Subject: [PATCH 199/261] Adding unset_related_userstory end point to epics API --- taiga/projects/epics/api.py | 25 +++++++++++ taiga/projects/epics/validators.py | 4 ++ .../test_epics_resources.py | 42 +++++++++++++++++++ tests/integration/test_epics.py | 22 +++++++++- 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index ea78c86a..c14e7792 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -276,6 +276,31 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.BadRequest(validator.errors) + @detail_route(methods=["POST"]) + def unset_related_userstory(self, request, **kwargs): + validator = validators.UnsetRelatedUserStoryValidator(data=request.DATA) + if validator.is_valid(): + data = validator.data + epic = self.get_object() + project = epic.project + user_story = UserStory.objects.get(id=data["us_id"]) + self.check_permissions(request, "update", epic) + + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) + + related_us = get_object_or_404( + models.RelatedUserStory, + epic=epic, + user_story=user_story + ) + related_us.delete() + epic = self.get_queryset().get(id=epic.id) + epic_serialized = self.get_serializer_class()(epic) + return response.Ok(epic_serialized.data) + + return response.BadRequest(validator.errors) + class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): permission_classes = (permissions.EpicVotersPermission,) diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 674d030f..975f1e4d 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -63,3 +63,7 @@ class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsVal class SetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): us_id = serializers.IntegerField() order = serializers.IntegerField(required=False, default=10000) + + +class UnsetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): + us_id = serializers.IntegerField() diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index 7b7f87ee..89b56bb5 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -159,6 +159,11 @@ def data(): m.private_us2 = f.UserStoryFactory(project=m.private_project2) m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + m.public_related_us = f.RelatedUserStory(epic=m.public_epic, user_story=m.public_us) + m.private_related_us1 = f.RelatedUserStory(epic=m.private_epic1, user_story=m.private_us1) + m.private_related_us2 = f.RelatedUserStory(epic=m.private_epic2, user_story=m.private_us2) + m.blocked_related_us = f.RelatedUserStory(epic=m.blocked_epic, user_story=m.blocked_us) + m.public_project.default_epic_status = m.public_epic.status m.public_project.save() m.private_project1.default_epic_status = m.private_epic1.status @@ -740,6 +745,43 @@ def test_set_related_user_story(client, data): assert results == [404, 404, 404, 451, 451] +def test_unset_related_user_story(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms + ] + + url = reverse('epics-unset-related-userstory', kwargs={"pk": data.public_epic.pk}) + edit_data = json.dumps({ + "us_id": data.public_related_us.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic1.pk}) + edit_data = json.dumps({ + "us_id": data.private_related_us1.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [401, 403, 403, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic2.pk}) + edit_data = json.dumps({ + "us_id": data.private_related_us2.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 200] + + url = reverse('epics-set-related-userstory', kwargs={"pk": data.blocked_epic.pk}) + edit_data = json.dumps({ + "us_id": data.blocked_related_us.user_story.pk, + }) + results = helper_test_http_method(client, 'post', url, edit_data, users) + assert results == [404, 404, 404, 451] + + def test_epic_action_upvote(client, data): public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 57a80d10..6d0e3107 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -133,7 +133,6 @@ def test_set_related_userstory_existing(client): us = f.UserStoryFactory.create() related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) - f.MembershipFactory.create(project=us.project, user=user, is_admin=True) url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) @@ -148,3 +147,24 @@ def test_set_related_userstory_existing(client): related_us = models.RelatedUserStory.objects.get(id=related_us.id) assert related_us.order == 77 + + +def test_unset_related_userstory(client): + user = f.UserFactory.create() + epic = f.EpicFactory.create() + us = f.UserStoryFactory.create() + related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) + f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + + url = reverse('epics-unset-related-userstory', kwargs={"pk": epic.pk}) + + data = { + "us_id": us.id + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 200 + assert response.data['user_stories_counts'] == {'opened': 0, 'closed': 0} + + assert not models.RelatedUserStory.objects.filter(id=related_us.id).exists() From 3656acd7f32d5fa0b0f9fb01a8d3a83feb3cd48a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 3 Aug 2016 09:40:33 +0200 Subject: [PATCH 200/261] Fixing tests --- tests/integration/test_epics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 6d0e3107..dd15a28d 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -133,6 +133,7 @@ def test_set_related_userstory_existing(client): us = f.UserStoryFactory.create() related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) + f.MembershipFactory.create(project=us.project, user=user, is_admin=True) url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) From bc69ecd8860a5d6ce589292d90987ed363b7b0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 3 Aug 2016 17:46:53 +0200 Subject: [PATCH 201/261] Improve userstories.validators --- taiga/projects/userstories/validators.py | 94 ++++++++++++++++------- tests/integration/test_userstories.py | 96 +++++++++++++++++------- 2 files changed, 133 insertions(+), 57 deletions(-) diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 9ff6b2f3..9f6780c8 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -20,16 +20,17 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers from taiga.base.api import validators -from taiga.base.api.utils import get_object_or_404 from taiga.base.exceptions import ValidationError from taiga.base.fields import PgArrayField from taiga.base.fields import PickledObjectField -from taiga.projects.milestones.validators import MilestoneExistsValidator -from taiga.projects.models import Project +from taiga.projects.milestones.models import Milestone +from taiga.projects.models import UserStoryStatus from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.userstories.models import UserStory +from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.validators import UserStoryStatusExistsValidator from . import models @@ -67,12 +68,22 @@ class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, v read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') -class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator, - validators.Validator): +class UserStoriesBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) bulk_stories = serializers.CharField() + def validate_status_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + if source in attrs: + filters["id"] = attrs[source] + + if not UserStoryStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid user story status id. The status must belong to " + "the same project.")) + + return attrs + # Order bulk validators @@ -88,20 +99,42 @@ class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatu milestone_id = serializers.IntegerField(required=False) bulk_stories = _UserStoryOrderBulkValidator(many=True) - def validate(self, data): - filters = {"project__id": data["project_id"]} - if "status_id" in data: - filters["status__id"] = data["status_id"] - if "milestone_id" in data: - filters["milestone__id"] = data["milestone_id"] + def validate_status_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + if source in attrs: + filters["id"] = attrs[source] - filters["id__in"] = [us["us_id"] for us in data["bulk_stories"]] + if not UserStoryStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid user story status id. The status must belong " + "to the same project.")) + + return attrs + + def validate_milestone_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + if source in attrs: + filters["id"] = attrs[source] + + if not Milestone.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid milestone id. The milistone must belong to the " + "same project.")) + + return attrs + + def validate_bulk_stories(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + if "status_id" in attrs: + filters["status__id"] = attrs["status_id"] + if "milestone_id" in attrs: + filters["milestone__id"] = attrs["milestone_id"] + + filters["id__in"] = [us["us_id"] for us in attrs[source]] if models.UserStory.objects.filter(**filters).count() != len(filters["id__in"]): - raise ValidationError(_("Invalid user story ids. All stories must belong to the same project and, " - "if it exists, to the same status and milestone.")) + raise ValidationError(_("Invalid user story ids. All stories must belong to the same project " + "and, if it exists, to the same status and milestone.")) - return data + return attrs # Milestone bulk validators @@ -111,22 +144,27 @@ class _UserStoryMilestoneBulkValidator(validators.Validator): order = serializers.IntegerField() -class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, validators.Validator): +class UpdateMilestoneBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() milestone_id = serializers.IntegerField() bulk_stories = _UserStoryMilestoneBulkValidator(many=True) - def validate(self, data): - """ - All the userstories and the milestone are from the same project - """ - user_story_ids = [us["us_id"] for us in data["bulk_stories"]] - project = get_object_or_404(Project, pk=data["project_id"]) + def validate_milestone_id(self, attrs, source): + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } + if not Milestone.objects.filter(**filters).exists(): + raise ValidationError(_("The milestone isn't valid for the project")) + return attrs - if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): + def validate_bulk_stories(self, attrs, source): + filters = { + "project__id": attrs["project_id"], + "id__in": [us["us_id"] for us in attrs[source]] + } + + if UserStory.objects.filter(**filters).count() != len(filters["id__in"]): raise ValidationError(_("All the user stories must be from the same project")) - if project.milestones.filter(id=data["milestone_id"]).count() != 1: - raise ValidationError(_("The milestone isn't valid for the project")) - - return data + return attrs diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index e158b802..57a2f520 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -157,6 +157,24 @@ def test_api_create_in_bulk_with_status(client): assert response.data[0]["status"] == project.default_us_status.id +def test_api_create_in_bulk_with_invalid_status(client): + project = f.create_project() + status = f.UserStoryStatusFactory.create() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("userstories-bulk-create") + data = { + "bulk_stories": "Story #1\nStory #2", + "project_id": project.id, + "status_id": status.id + } + + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400, response.data + assert "status_id" in response.data + + def test_api_update_orders_in_bulk(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) @@ -175,13 +193,14 @@ def test_api_update_orders_in_bulk(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) - response3 = client.json.post(url3, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 200, response.data - assert response1.status_code == 200, response1.data - assert response2.status_code == 200, response2.data - assert response3.status_code == 200, response3.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 200, response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 200, response.data def test_api_update_orders_in_bulk_invalid_userstories(client): @@ -204,19 +223,24 @@ def test_api_update_orders_in_bulk_invalid_userstories(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) - response3 = client.json.post(url3, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_stories" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data - assert response3.status_code == 400, response3.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_stories" in response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_stories" in response.data def test_api_update_orders_in_bulk_invalid_status(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) - us1 = f.create_userstory(project=project) + status = f.UserStoryStatusFactory.create() + us1 = f.create_userstory(project=project, status=status) us2 = f.create_userstory(project=project, status=us1.status) us3 = f.create_userstory(project=project) @@ -226,7 +250,7 @@ def test_api_update_orders_in_bulk_invalid_status(client): data = { "project_id": project.id, - "status_id": us1.status.id, + "status_id": status.id, "bulk_stories": [{"us_id": us1.id, "order": 1}, {"us_id": us2.id, "order": 2}, {"us_id": us3.id, "order": 3}] @@ -234,19 +258,26 @@ def test_api_update_orders_in_bulk_invalid_status(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) - response3 = client.json.post(url3, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_stories" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data - assert response3.status_code == 400, response3.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_stories" in response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_stories" in response.data def test_api_update_orders_in_bulk_invalid_milestione(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) - mil1 = f.MilestoneFactory.create(project=project) + mil1 = f.MilestoneFactory.create() us1 = f.create_userstory(project=project, milestone=mil1) us2 = f.create_userstory(project=project, milestone=mil1) us3 = f.create_userstory(project=project) @@ -265,13 +296,20 @@ def test_api_update_orders_in_bulk_invalid_milestione(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) - response3 = client.json.post(url3, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_stories" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data - assert response3.status_code == 400, response3.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_stories" in response.data + + response = client.json.post(url3, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_stories" in response.data def test_api_update_milestone_in_bulk(client): @@ -322,7 +360,7 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert len(response.data["non_field_errors"]) == 1 + assert "milestone_id" in response.data def test_api_update_milestone_in_bulk_invalid_userstories(client): @@ -344,7 +382,7 @@ def test_api_update_milestone_in_bulk_invalid_userstories(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert len(response.data["non_field_errors"]) == 1 + assert "bulk_stories" in response.data def test_update_userstory_points(client): From abff91e6bbe601866d34a211cc31a36df6570ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 4 Aug 2016 10:31:38 +0200 Subject: [PATCH 202/261] Fix memeverships-bulk-create validator --- taiga/projects/validators.py | 17 ++++++++++--- taiga/users/validators.py | 10 +------- .../test_projects_choices_resources.py | 1 + tests/integration/test_memberships.py | 24 +++++++++++++++++++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index fdf917ea..7d3c48cc 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -24,7 +24,7 @@ from taiga.base.api import validators from taiga.base.exceptions import ValidationError from taiga.base.fields import JsonField from taiga.base.fields import PgArrayField -from taiga.users.validators import RoleExistsValidator +from taiga.users.models import Role from .tagging.fields import TagsField @@ -200,16 +200,27 @@ class MembershipAdminValidator(MembershipValidator): exclude = ("token",) -class MemberBulkValidator(RoleExistsValidator, validators.Validator): +class _MemberBulkValidator(validators.Validator): email = serializers.EmailField() role_id = serializers.IntegerField() class MembersBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() - bulk_memberships = MemberBulkValidator(many=True) + bulk_memberships = _MemberBulkValidator(many=True) invitation_extra_text = serializers.CharField(required=False, max_length=255) + def validate_bulk_memberships(self, attrs, source): + filters = { + "project__id": attrs["project_id"], + "id__in": [r["role_id"] for r in attrs["bulk_memberships"]] + } + + if Role.objects.filter(**filters).count() != len(set(filters["id__in"])): + raise ValidationError(_("Invalid role ids. All roles must belong to the same project.")) + + return attrs + ###################################################### # Projects diff --git a/taiga/users/validators.py b/taiga/users/validators.py index f23da47a..279e6ce4 100644 --- a/taiga/users/validators.py +++ b/taiga/users/validators.py @@ -29,18 +29,10 @@ from .models import User, Role import re -class RoleExistsValidator: - def validate_role_id(self, attrs, source): - value = attrs[source] - if not Role.objects.filter(pk=value).exists(): - msg = _("There's no role with that id") - raise ValidationError(msg) - return attrs - - ###################################################### # User ###################################################### + class UserValidator(validators.ModelValidator): class Meta: model = User diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index 75c0f39d..28e1f483 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -2055,6 +2055,7 @@ def test_membership_action_bulk_create(client, data): results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 403, 451] + def test_membership_action_resend_invitation(client, data): public_invitation = f.InvitationFactory(project=data.public_project, role__project=data.public_project) private_invitation1 = f.InvitationFactory(project=data.private_project1, role__project=data.private_project1) diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index c6b2ca5e..70d3d198 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -72,6 +72,30 @@ def test_api_create_bulk_members(client): assert response.data[1]["email"] == joseph.email +def test_api_create_bulk_members_with_invalid_roles(client): + project = f.ProjectFactory() + john = f.UserFactory.create() + joseph = f.UserFactory.create() + tester = f.RoleFactory(name="Tester") + gamer = f.RoleFactory(name="Gamer") + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": tester.pk, "email": john.email}, + {"role_id": gamer.pk, "email": joseph.email}, + ] + } + client.login(project.owner) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "bulk_memberships" in response.data + + def test_api_create_bulk_members_with_allowed_domain(client): project = f.ProjectFactory() john = f.UserFactory.create() From 79f74135f76fc3ae11ac6a760e85e838a26af482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 4 Aug 2016 12:14:14 +0200 Subject: [PATCH 203/261] Remove unneeded code --- taiga/projects/userstories/validators.py | 4 +--- taiga/projects/validators.py | 19 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 9f6780c8..bc52db1a 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -30,7 +30,6 @@ from taiga.projects.notifications.validators import WatchersValidator from taiga.projects.tagging.fields import TagsAndTagsColorsField from taiga.projects.userstories.models import UserStory from taiga.projects.validators import ProjectExistsValidator -from taiga.projects.validators import UserStoryStatusExistsValidator from . import models @@ -92,8 +91,7 @@ class _UserStoryOrderBulkValidator(validators.Validator): order = serializers.IntegerField() -class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator, - validators.Validator): +class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) milestone_id = serializers.IntegerField(required=False) diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index 7d3c48cc..54a43178 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -33,7 +33,6 @@ from . import services class DuplicatedNameInProjectValidator: - def validate_name(self, attrs, source): """ Check the points name is not duplicated in the project on creation @@ -64,24 +63,6 @@ class ProjectExistsValidator: return attrs -class UserStoryStatusExistsValidator: - def validate_status_id(self, attrs, source): - value = attrs[source] - if not models.UserStoryStatus.objects.filter(pk=value).exists(): - msg = _("There's no user story status with that id") - raise ValidationError(msg) - return attrs - - -class TaskStatusExistsValidator: - def validate_status_id(self, attrs, source): - value = attrs[source] - if not models.TaskStatus.objects.filter(pk=value).exists(): - msg = _("There's no task status with that id") - raise ValidationError(msg) - return attrs - - ###################################################### # Custom values for selectors ###################################################### From 890c668e7e364914427df06209737c82aacf7c5b Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 4 Aug 2016 09:50:24 +0200 Subject: [PATCH 204/261] WIP: epics admin --- taiga/projects/epics/admin.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 taiga/projects/epics/admin.py diff --git a/taiga/projects/epics/admin.py b/taiga/projects/epics/admin.py new file mode 100644 index 00000000..af299f37 --- /dev/null +++ b/taiga/projects/epics/admin.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.contrib import admin + +from taiga.projects.notifications.admin import WatchedInline +from taiga.projects.votes.admin import VoteInline + +from . import models + + +class EpicAdmin(admin.ModelAdmin): + list_display = ["project", "ref", "subject"] + list_display_links = ["ref", "subject"] + inlines = [WatchedInline, VoteInline] + raw_id_fields = ["project"] + search_fields = ["subject", "description", "id", "ref"] + + def get_object(self, *args, **kwargs): + self.obj = super().get_object(*args, **kwargs) + return self.obj + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if (db_field.name in ["status"] and getattr(self, 'obj', None)): + kwargs["queryset"] = db_field.related.model.objects.filter(project=self.obj.project) + + elif (db_field.name in ["owner", "assigned_to"] and getattr(self, 'obj', None)): + kwargs["queryset"] = db_field.related.model.objects.filter(memberships__project=self.obj.project) + + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def formfield_for_manytomany(self, db_field, request, **kwargs): + if (db_field.name in ["watchers"] and getattr(self, 'obj', None)): + kwargs["queryset"] = db_field.related.parent_model.objects.filter(memberships__project=self.obj.project) + return super().formfield_for_manytomany(db_field, request, **kwargs) + + +admin.site.register(models.Epic, EpicAdmin) From 32267af4f4acb7a80ed934e3d41ea0d93d740e88 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 4 Aug 2016 14:08:34 +0200 Subject: [PATCH 205/261] Refactoring epics API --- taiga/base/api/viewsets.py | 19 +++ taiga/base/routers.py | 53 ++++++- taiga/projects/epics/admin.py | 9 +- taiga/projects/epics/api.py | 131 +++++++++--------- taiga/projects/epics/models.py | 2 +- taiga/projects/epics/permissions.py | 14 +- taiga/projects/epics/serializers.py | 5 + taiga/projects/epics/validators.py | 11 +- taiga/routers.py | 8 +- .../test_epics_resources.py | 12 +- tests/integration/test_epics.py | 9 +- 11 files changed, 183 insertions(+), 90 deletions(-) diff --git a/taiga/base/api/viewsets.py b/taiga/base/api/viewsets.py index 95b09055..d37bfc50 100644 --- a/taiga/base/api/viewsets.py +++ b/taiga/base/api/viewsets.py @@ -134,6 +134,25 @@ class ViewSetMixin(object): return super().check_permissions(request, action=action, obj=obj) +class NestedViewSetMixin(object): + def get_queryset(self): + return self._filter_queryset_by_parents_lookups(super().get_queryset()) + + def _filter_queryset_by_parents_lookups(self, queryset): + parents_query_dict = self._get_parents_query_dict() + if parents_query_dict: + return queryset.filter(**parents_query_dict) + else: + return queryset + + def _get_parents_query_dict(self): + result = {} + for kwarg_name in self.kwargs: + query_value = self.kwargs.get(kwarg_name) + result[kwarg_name] = query_value + return result + + class ViewSet(ViewSetMixin, views.APIView): """ The base ViewSet class does not provide any actions by default. diff --git a/taiga/base/routers.py b/taiga/base/routers.py index 56b80f8e..a7ccbdc4 100644 --- a/taiga/base/routers.py +++ b/taiga/base/routers.py @@ -318,7 +318,58 @@ class DRFDefaultRouter(SimpleRouter): return urls -class DefaultRouter(DRFDefaultRouter): +class NestedRegistryItem(object): + def __init__(self, router, parent_prefix, parent_item=None): + self.router = router + self.parent_prefix = parent_prefix + self.parent_item = parent_item + + def register(self, prefix, viewset, base_name, parents_query_lookups): + self.router._register( + prefix=self.get_prefix(current_prefix=prefix, parents_query_lookups=parents_query_lookups), + viewset=viewset, + base_name=base_name, + ) + return NestedRegistryItem( + router=self.router, + parent_prefix=prefix, + parent_item=self + ) + + def get_prefix(self, current_prefix, parents_query_lookups): + return "{0}/{1}".format( + self.get_parent_prefix(parents_query_lookups), + current_prefix + ) + + def get_parent_prefix(self, parents_query_lookups): + prefix = "/" + current_item = self + i = len(parents_query_lookups) - 1 + while current_item: + prefix = "{parent_prefix}/(?P<{parent_pk_kwarg_name}>[^/.]+)/{prefix}".format( + parent_prefix=current_item.parent_prefix, + parent_pk_kwarg_name=parents_query_lookups[i], + prefix=prefix + ) + i -= 1 + current_item = current_item.parent_item + return prefix.strip("/") + + +class NestedRouterMixin: + def _register(self, *args, **kwargs): + return super().register(*args, **kwargs) + + def register(self, *args, **kwargs): + self._register(*args, **kwargs) + return NestedRegistryItem( + router=self, + parent_prefix=self.registry[-1][0] + ) + + +class DefaultRouter(NestedRouterMixin, DRFDefaultRouter): pass __all__ = ["DefaultRouter"] diff --git a/taiga/projects/epics/admin.py b/taiga/projects/epics/admin.py index af299f37..69aea806 100644 --- a/taiga/projects/epics/admin.py +++ b/taiga/projects/epics/admin.py @@ -24,10 +24,17 @@ from taiga.projects.votes.admin import VoteInline from . import models +class RelatedUserStoriesInline(admin.TabularInline): + model = models.RelatedUserStory + sortable_field_name = "order" + raw_id_fields = ["user_story", ] + extra = 0 + + class EpicAdmin(admin.ModelAdmin): list_display = ["project", "ref", "subject"] list_display_links = ["ref", "subject"] - inlines = [WatchedInline, VoteInline] + inlines = [WatchedInline, VoteInline, RelatedUserStoriesInline] raw_id_fields = ["project"] search_fields = ["subject", "description", "id", "ref"] diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index c14e7792..f98e4303 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -25,6 +25,7 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route, detail_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin +from taiga.base.api.viewsets import NestedViewSetMixin from taiga.base.utils import json from taiga.projects.history.mixins import HistoryResourceMixin @@ -108,17 +109,16 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, super().pre_save(obj) - def _reorder_if_needed(self, obj, old_order_key, order_key, order_attr, project): + def _reorder_if_needed(self, obj, old_order_key, order_key): # Executes the extra ordering if there is a difference in the ordering keys if old_order_key != order_key: extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) - data = [{"epic_id": obj.id, "order": getattr(obj, order_attr)}] + data = [{"epic_id": obj.id, "order": getattr(obj, "epics_order")}] for id, order in extra_orders.items(): data.append({"epic_id": int(id), "order": order}) return services.update_epics_order_in_bulk(data, - field=order_attr, - project=project) + project=obj.project) return {} def post_save(self, obj, created=False): @@ -126,9 +126,7 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, # Let's reorder the related stuff after edit the element orders_updated = self._reorder_if_needed(obj, self._old_epics_order_key, - self._epics_order_key(obj), - "epics_order", - obj.project) + self._epics_order_key(obj)) self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated) super().post_save(obj, created) @@ -227,77 +225,78 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.BadRequest(validator.errors) - @detail_route(methods=["POST"]) - def bulk_create_related_userstories(self, request, **kwargs): + +class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, ModelCrudViewSet): + queryset = models.RelatedUserStory.objects.all() + serializer_class = serializers.EpicRelatedUserStorySerializer + validator_class = validators.EpicRelatedUserStoryValidator + model = models.RelatedUserStory + permission_classes = (permissions.EpicRelatedUserStoryPermission,) + + """ + Updating the order attribute can affect the ordering of another userstories in the epic + This method generate a key for the userstory and can be used to be compared before and after + saving + If there is any difference it means an extra ordering update must be done + """ + def _order_key(self, obj): + return "{}-{}".format(obj.user_story.project_id, obj.order) + + def pre_save(self, obj): + if not obj.id: + obj.epic_id = self.kwargs["epic"] + else: + self._old_order_key = self._order_key(self.get_object()) + + super().pre_save(obj) + + def _reorder_if_needed(self, obj, old_order_key, order_key): + # Executes the extra ordering if there is a difference in the ordering keys + if old_order_key != order_key: + extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) + data = [{"us_id": obj.id, "order": getattr(obj, "order")}] + for id, order in extra_orders.items(): + data.append({"epic_id": int(id), "order": order}) + + return services.update_epic_related_userstories_order_in_bulk( + data, + epic=obj.epic + ) + + return {} + + def post_save(self, obj, created=False): + if not created: + # Let's reorder the related stuff after edit the element + orders_updated = self._reorder_if_needed(obj, + self._old_epics_order_key, + self._epics_order_key(obj)) + self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated) + + super().post_save(obj, created) + + @list_route(methods=["POST"]) + def bulk_create(self, request, **kwargs): validator = validators.CrateRelatedUserStoriesBulkValidator(data=request.DATA) if validator.is_valid(): data = validator.data - obj = self.get_object() - project = obj.project - self.check_permissions(request, 'bulk_create_userstories', project) + + epic = get_object_or_404(models.Epic, id=kwargs["epic"]) + project = epic.project + + self.check_permissions(request, 'bulk_create', project) if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) services.create_related_userstories_in_bulk( data["userstories"], - obj, + epic, project=project, owner=request.user ) - obj = self.get_queryset().get(id=obj.id) - epic_serialized = self.get_serializer_class()(obj) - return response.Ok(epic_serialized.data) - return response.BadRequest(validator.errors) - - @detail_route(methods=["POST"]) - def set_related_userstory(self, request, **kwargs): - validator = validators.SetRelatedUserStoryValidator(data=request.DATA) - if validator.is_valid(): - data = validator.data - epic = self.get_object() - project = epic.project - user_story = UserStory.objects.get(id=data["us_id"]) - self.check_permissions(request, "update", epic) - self.check_permissions(request, "select_related_userstory", user_story.project) - - if project.blocked_code is not None: - raise exc.Blocked(_("Blocked element")) - - obj, created = models.RelatedUserStory.objects.update_or_create( - epic=epic, - user_story=user_story, - defaults={ - "order": data["order"] - }) - epic = self.get_queryset().get(id=epic.id) - epic_serialized = self.get_serializer_class()(epic) - return response.Ok(epic_serialized.data) - - return response.BadRequest(validator.errors) - - @detail_route(methods=["POST"]) - def unset_related_userstory(self, request, **kwargs): - validator = validators.UnsetRelatedUserStoryValidator(data=request.DATA) - if validator.is_valid(): - data = validator.data - epic = self.get_object() - project = epic.project - user_story = UserStory.objects.get(id=data["us_id"]) - self.check_permissions(request, "update", epic) - - if project.blocked_code is not None: - raise exc.Blocked(_("Blocked element")) - - related_us = get_object_or_404( - models.RelatedUserStory, - epic=epic, - user_story=user_story - ) - related_us.delete() - epic = self.get_queryset().get(id=epic.id) - epic_serialized = self.get_serializer_class()(epic) - return response.Ok(epic_serialized.data) + related_uss_serialized = self.get_serializer_class()(epic.relateduserstory_set.all(), many=True) + return response.Ok(related_uss_serialized.data) return response.BadRequest(validator.errors) diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index eb18c6c4..de501ffc 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -105,4 +105,4 @@ class RelatedUserStory(models.Model): ordering = ["user_story", "order", "id"] def __str__(self): - return "{0} - {1}".format(self.epic, self.user_story) + return "{0} - {1}".format(self.epic_id, self.user_story_id) diff --git a/taiga/projects/epics/permissions.py b/taiga/projects/epics/permissions.py index 3cd31b07..fd473e18 100644 --- a/taiga/projects/epics/permissions.py +++ b/taiga/projects/epics/permissions.py @@ -34,14 +34,24 @@ class EpicPermission(TaigaResourcePermission): filters_data_perms = AllowAny() csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_epic') - bulk_create_userstories_perms = HasProjectPerm('modify_epic') & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) - select_related_userstory_perms = HasProjectPerm('view_us') upvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') downvote_perms = IsAuthenticated() & HasProjectPerm('view_epics') watch_perms = IsAuthenticated() & HasProjectPerm('view_epics') unwatch_perms = IsAuthenticated() & HasProjectPerm('view_epics') +class EpicRelatedUserStoryPermission(TaigaResourcePermission): + enought_perms = IsProjectAdmin() | IsSuperUser() + global_perms = None + retrieve_perms = HasProjectPerm('view_epics') + create_perms = HasProjectPerm('modify_epic') + update_perms = HasProjectPerm('modify_epic') + partial_update_perms = HasProjectPerm('modify_epic') + destroy_perms = HasProjectPerm('modify_epic') + list_perms = AllowAny() + bulk_create_perms = HasProjectPerm('modify_epic') + + class EpicVotersPermission(TaigaResourcePermission): enought_perms = IsProjectAdmin() | IsSuperUser() global_perms = None diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py index 9b845bbe..29a94348 100644 --- a/taiga/projects/epics/serializers.py +++ b/taiga/projects/epics/serializers.py @@ -78,3 +78,8 @@ class EpicSerializer(EpicListSerializer): class EpicNeighborsSerializer(NeighborsSerializerMixin, EpicSerializer): pass + + +class EpicRelatedUserStorySerializer(serializers.LightSerializer): + user_story = Field(attr="user_story_id") + order = Field() diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 975f1e4d..88214aa6 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -60,10 +60,7 @@ class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsVal userstories = serializers.CharField() -class SetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): - us_id = serializers.IntegerField() - order = serializers.IntegerField(required=False, default=10000) - - -class UnsetRelatedUserStoryValidator(UserStoryExistsValidator, validators.Validator): - us_id = serializers.IntegerField() +class EpicRelatedUserStoryValidator(validators.ModelValidator): + class Meta: + model = models.RelatedUserStory + read_only_fields = ('id', 'epic', 'user_story') diff --git a/taiga/routers.py b/taiga/routers.py index e0d0baa1..92f2d58c 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -146,6 +146,7 @@ from taiga.projects.milestones.api import MilestoneViewSet from taiga.projects.milestones.api import MilestoneWatchersViewSet from taiga.projects.epics.api import EpicViewSet +from taiga.projects.epics.api import EpicRelatedUserStoryViewSet from taiga.projects.epics.api import EpicVotersViewSet from taiga.projects.epics.api import EpicWatchersViewSet @@ -170,8 +171,11 @@ router.register(r"milestones", MilestoneViewSet, router.register(r"milestones/(?P\d+)/watchers", MilestoneWatchersViewSet, base_name="milestone-watchers") -router.register(r"epics", EpicViewSet, - base_name="epics") +router.register(r"epics", EpicViewSet, base_name="epics")\ + .register(r"related_userstories", EpicRelatedUserStoryViewSet, + base_name="epics-related-userstories", + parents_query_lookups=["epic"]) + router.register(r"epics/(?P\d+)/voters", EpicVotersViewSet, base_name="epic-voters") router.register(r"epics/(?P\d+)/watchers", EpicWatchersViewSet, diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index 89b56bb5..dff20c09 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -676,10 +676,10 @@ def test_epic_action_bulk_create(client, data): def test_bulk_create_related_userstories(client, data): - public_url = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.public_epic.pk}) - private_url1 = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.private_epic1.pk}) - private_url2 = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.private_epic2.pk}) - blocked_url = reverse('epics-bulk-create-related-userstories', kwargs={"pk": data.blocked_epic.pk}) + public_url = reverse('epics-related-userstories-bulk-create', args=[data.public_epic.pk]) + private_url1 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic1.pk]) + private_url2 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic2.pk]) + blocked_url = reverse('epics-related-userstories-bulk-create', args=[data.blocked_epic.pk]) users = [ None, @@ -698,9 +698,9 @@ def test_bulk_create_related_userstories(client, data): results = helper_test_http_method(client, 'post', private_url1, bulk_data, users) assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'post', private_url2, bulk_data, users) - assert results == [404, 404, 404, 200, 200] + assert results == [401, 403, 403, 200, 200] results = helper_test_http_method(client, 'post', blocked_url, bulk_data, users) - assert results == [404, 404, 404, 451, 451] + assert results == [401, 403, 403, 451, 451] def test_set_related_user_story(client, data): diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index dd15a28d..01f1e483 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -98,7 +98,7 @@ def test_bulk_create_related_userstories(client): epic = f.EpicFactory.create(project=project) f.MembershipFactory.create(project=project, user=user, is_admin=True) - url = reverse('epics-bulk-create-related-userstories', kwargs={"pk": epic.pk}) + url = reverse('epics-related-userstories-bulk-create', args=[epic.pk]) data = { "userstories": "test1\ntest2" @@ -106,7 +106,7 @@ def test_bulk_create_related_userstories(client): client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 200 - assert response.data['user_stories_counts'] == {'opened': 2, 'closed': 0} + assert len(response.data) == 2 def test_set_related_userstory(client): @@ -116,13 +116,14 @@ def test_set_related_userstory(client): f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) f.MembershipFactory.create(project=us.project, user=user, is_admin=True) - url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) + url = reverse('epics-related-userstories-list', args=[epic.pk]) data = { - "us_id": us.id + "user_story": us.id } client.login(user) response = client.json.post(url, json.dumps(data)) + print(response.data) assert response.status_code == 200 assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} From e83e4b8beb7c2d2306fbf9c3e951bb46b903afa6 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 4 Aug 2016 14:33:28 +0200 Subject: [PATCH 206/261] Refactoring epics API --- taiga/projects/epics/api.py | 1 + taiga/projects/epics/models.py | 4 ++++ tests/integration/test_epics.py | 16 +++++----------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index f98e4303..242b3ebe 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -232,6 +232,7 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod validator_class = validators.EpicRelatedUserStoryValidator model = models.RelatedUserStory permission_classes = (permissions.EpicRelatedUserStoryPermission,) + lookup_field = "user_story_id" """ Updating the order attribute can affect the ordering of another userstories in the epic diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index de501ffc..3d1a2ce4 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -106,3 +106,7 @@ class RelatedUserStory(models.Model): def __str__(self): return "{0} - {1}".format(self.epic_id, self.user_story_id) + + @property + def project(self): + return self.epic.project diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 01f1e483..5d4657df 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -136,10 +136,10 @@ def test_set_related_userstory_existing(client): f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) f.MembershipFactory.create(project=us.project, user=user, is_admin=True) - url = reverse('epics-set-related-userstory', kwargs={"pk": epic.pk}) + url = reverse('epics-related-userstories-list', args=[epic.pk]) data = { - "us_id": us.id, + "user_story": us.id, "order": 77 } client.login(user) @@ -158,15 +158,9 @@ def test_unset_related_userstory(client): related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) - url = reverse('epics-unset-related-userstory', kwargs={"pk": epic.pk}) + url = reverse('epics-related-userstories-detail', args=[epic.pk, us.pk]) - data = { - "us_id": us.id - } client.login(user) - response = client.json.post(url, json.dumps(data)) - print(response.data) - assert response.status_code == 200 - assert response.data['user_stories_counts'] == {'opened': 0, 'closed': 0} - + response = client.delete(url) + assert response.status_code == 204 assert not models.RelatedUserStory.objects.filter(id=related_us.id).exists() From 4c6f49aaabfa507739a66ea053a22f83fdfce605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 4 Aug 2016 17:13:30 +0200 Subject: [PATCH 207/261] Improve more validators --- taiga/projects/tasks/api.py | 6 +- taiga/projects/tasks/validators.py | 81 +++++++--- taiga/projects/userstories/validators.py | 39 ++--- tests/integration/test_tasks.py | 197 +++++++++++++++++++---- 4 files changed, 254 insertions(+), 69 deletions(-) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index bc76f24f..9584a17b 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -179,8 +179,8 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "destroy", self.object) self.check_permissions(request, "create", new_project) - sprint_id = request.DATA.get('milestone', None) - if sprint_id is not None and new_project.milestones.filter(pk=sprint_id).count() == 0: + milestone_id = request.DATA.get('milestone', None) + if milestone_id is not None and new_project.milestones.filter(pk=milestone_id).count() == 0: request.DATA['milestone'] = None us_id = request.DATA.get('user_story', None) @@ -259,7 +259,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, raise exc.Blocked(_("Blocked element")) tasks = services.create_tasks_in_bulk( - data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], + data["bulk_tasks"], milestone_id=data["milestone_id"], user_story_id=data["us_id"], status_id=data.get("status_id") or project.default_task_status_id, project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index 033b72e3..b9061bde 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -30,7 +30,6 @@ from taiga.projects.tagging.fields import TagsAndTagsColorsField from taiga.projects.userstories.models import UserStory from taiga.projects.validators import ProjectExistsValidator - from . import models @@ -45,23 +44,27 @@ class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, valida class TasksBulkValidator(ProjectExistsValidator, validators.Validator): project_id = serializers.IntegerField() - sprint_id = serializers.IntegerField() + milestone_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) bulk_tasks = serializers.CharField() - def validate_sprint_id(self, attrs, source): - filters = {"project__id": attrs["project_id"]} - filters["id"] = attrs["sprint_id"] + def validate_milestone_id(self, attrs, source): + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } if not Milestone.objects.filter(**filters).exists(): - raise ValidationError(_("Invalid sprint id.")) + raise ValidationError(_("Invalid milestone id.")) return attrs def validate_status_id(self, attrs, source): - filters = {"project__id": attrs["project_id"]} - filters["id"] = attrs["status_id"] + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } if not TaskStatus.objects.filter(**filters).exists(): raise ValidationError(_("Invalid task status id.")) @@ -71,13 +74,13 @@ class TasksBulkValidator(ProjectExistsValidator, validators.Validator): def validate_us_id(self, attrs, source): filters = {"project__id": attrs["project_id"]} - if "sprint_id" in attrs: - filters["milestone__id"] = attrs["sprint_id"] + if "milestone_id" in attrs: + filters["milestone__id"] = attrs["milestone_id"] filters["id"] = attrs["us_id"] if not UserStory.objects.filter(**filters).exists(): - raise ValidationError(_("Invalid sprint id.")) + raise ValidationError(_("Invalid user story id.")) return attrs @@ -96,19 +99,55 @@ class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator milestone_id = serializers.IntegerField(required=False) bulk_tasks = _TaskOrderBulkValidator(many=True) - def validate(self, data): - filters = {"project__id": data["project_id"]} - if "status_id" in data: - filters["status__id"] = data["status_id"] - if "us_id" in data: - filters["user_story__id"] = data["us_id"] - if "milestone_id" in data: - filters["milestone__id"] = data["milestone_id"] + def validate_status_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + filters["id"] = attrs[source] - filters["id__in"] = [t["task_id"] for t in data["bulk_tasks"]] + if not TaskStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid task status id. The status must belong to " + "the same project.")) + + return attrs + + def validate_us_id(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + + if "milestone_id" in attrs: + filters["milestone__id"] = attrs["milestone_id"] + + filters["id"] = attrs[source] + + if not UserStory.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid user story id. The user story must belong to " + "the same project.")) + + return attrs + + def validate_milestone_id(self, attrs, source): + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } + + if not Milestone.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid milestone id. The milestone must belong to " + "the same project.")) + + return attrs + + def validate_bulk_tasks(self, attrs, source): + filters = {"project__id": attrs["project_id"]} + if "status_id" in attrs: + filters["status__id"] = attrs["status_id"] + if "us_id" in attrs: + filters["user_story__id"] = attrs["us_id"] + if "milestone_id" in attrs: + filters["milestone__id"] = attrs["milestone_id"] + + filters["id__in"] = [t["task_id"] for t in attrs[source]] if models.Task.objects.filter(**filters).count() != len(filters["id__in"]): raise ValidationError(_("Invalid task ids. All tasks must belong to the same project and, " "if it exists, to the same status, user story and/or milestone.")) - return data + return attrs diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index bc52db1a..e20a704e 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -73,13 +73,14 @@ class UserStoriesBulkValidator(ProjectExistsValidator, validators.Validator): bulk_stories = serializers.CharField() def validate_status_id(self, attrs, source): - filters = {"project__id": attrs["project_id"]} - if source in attrs: - filters["id"] = attrs[source] + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } - if not UserStoryStatus.objects.filter(**filters).exists(): - raise ValidationError(_("Invalid user story status id. The status must belong to " - "the same project.")) + if not UserStoryStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid user story status id. The status must belong to " + "the same project.")) return attrs @@ -98,24 +99,26 @@ class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, validators.Val bulk_stories = _UserStoryOrderBulkValidator(many=True) def validate_status_id(self, attrs, source): - filters = {"project__id": attrs["project_id"]} - if source in attrs: - filters["id"] = attrs[source] + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } - if not UserStoryStatus.objects.filter(**filters).exists(): - raise ValidationError(_("Invalid user story status id. The status must belong " - "to the same project.")) + if not UserStoryStatus.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid user story status id. The status must belong " + "to the same project.")) return attrs def validate_milestone_id(self, attrs, source): - filters = {"project__id": attrs["project_id"]} - if source in attrs: - filters["id"] = attrs[source] + filters = { + "project__id": attrs["project_id"], + "id": attrs[source] + } - if not Milestone.objects.filter(**filters).exists(): - raise ValidationError(_("Invalid milestone id. The milistone must belong to the " - "same project.")) + if not Milestone.objects.filter(**filters).exists(): + raise ValidationError(_("Invalid milestone id. The milistone must belong to the " + "same project.")) return attrs diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index c9fd4d01..04546233 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -104,7 +104,7 @@ def test_api_create_in_bulk_with_status_milestone_userstory(client): "bulk_tasks": "Story #1\nStory #2", "us_id": us.id, "project_id": us.project.id, - "sprint_id": us.milestone.id, + "milestone_id": us.milestone.id, "status_id": us.project.default_task_status.id } @@ -129,7 +129,7 @@ def test_api_create_in_bulk_with_status_milestone(client): data = { "bulk_tasks": "Story #1\nStory #2", "project_id": us.project.id, - "sprint_id": us.milestone.id, + "milestone_id": us.milestone.id, "status_id": us.project.default_task_status.id } @@ -158,7 +158,7 @@ def test_api_create_in_bulk_with_invalid_status(client): "bulk_tasks": "Story #1\nStory #2", "us_id": us.id, "project_id": project.id, - "sprint_id": milestone.id, + "milestone_id": milestone.id, "status_id": status.id } @@ -166,6 +166,7 @@ def test_api_create_in_bulk_with_invalid_status(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + assert "status_id" in response.data def test_api_create_in_bulk_with_invalid_milestone(client): @@ -183,7 +184,7 @@ def test_api_create_in_bulk_with_invalid_milestone(client): "bulk_tasks": "Story #1\nStory #2", "us_id": us.id, "project_id": project.id, - "sprint_id": milestone.id, + "milestone_id": milestone.id, "status_id": project.default_task_status.id } @@ -191,6 +192,7 @@ def test_api_create_in_bulk_with_invalid_milestone(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + assert "milestone_id" in response.data def test_api_create_in_bulk_with_invalid_userstory_1(client): @@ -208,7 +210,7 @@ def test_api_create_in_bulk_with_invalid_userstory_1(client): "bulk_tasks": "Story #1\nStory #2", "us_id": us.id, "project_id": project.id, - "sprint_id": milestone.id, + "milestone_id": milestone.id, "status_id": project.default_task_status.id } @@ -216,6 +218,7 @@ def test_api_create_in_bulk_with_invalid_userstory_1(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + assert "us_id" in response.data def test_api_create_in_bulk_with_invalid_userstory_2(client): @@ -233,7 +236,7 @@ def test_api_create_in_bulk_with_invalid_userstory_2(client): "bulk_tasks": "Story #1\nStory #2", "us_id": us.id, "project_id": us.project.id, - "sprint_id": milestone.id, + "milestone_id": milestone.id, "status_id": us.project.default_task_status.id } @@ -241,6 +244,7 @@ def test_api_create_in_bulk_with_invalid_userstory_2(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + assert "us_id" in response.data def test_api_create_invalid_task(client): @@ -309,14 +313,17 @@ def test_api_update_order_in_bulk_invalid_tasks(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data -def test_api_update_order_in_bulk_invalid_status(client): + +def test_api_update_order_in_bulk_invalid_tasks_for_status(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) task1 = f.create_task(project=project) @@ -336,14 +343,16 @@ def test_api_update_order_in_bulk_invalid_status(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data -def test_api_update_order_in_bulk_invalid_milestone(client): +def test_api_update_order_in_bulk_invalid_tasks_for_milestone(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) mil1 = f.MilestoneFactory.create(project=project) @@ -364,19 +373,21 @@ def test_api_update_order_in_bulk_invalid_milestone(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data -def test_api_update_order_in_bulk_invalid_user_story(client): +def test_api_update_order_in_bulk_invalid_tasks_for_user_story(client): project = f.create_project() f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) us1 = f.create_userstory(project=project) - task1 = f.create_task(project=project, user_story=us1) - task2 = f.create_task(project=project, user_story=us1) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) task3 = f.create_task(project=project) url1 = reverse("tasks-bulk-update-taskboard-order") @@ -392,11 +403,143 @@ def test_api_update_order_in_bulk_invalid_user_story(client): client.login(project.owner) - response1 = client.json.post(url1, json.dumps(data)) - response2 = client.json.post(url2, json.dumps(data)) + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data - assert response1.status_code == 400, response1.data - assert response2.status_code == 400, response2.data + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_status(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + status = f.TaskStatusFactory.create() + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "status_id": status.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "status_id" in response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_milestone(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + mil1 = f.MilestoneFactory.create() + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "milestone_id": mil1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "milestone_id" in response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_user_story_1(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + us1 = f.create_userstory() + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data + + +def test_api_update_order_in_bulk_invalid_user_story_2(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + milestone = f.MilestoneFactory.create(project=project) + us1 = f.create_userstory(project=project) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + task3 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "us_id": us1.id, + "milestone_id": milestone.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}, + {"task_id": task3.id, "order": 3}] + } + + client.login(project.owner) + + response = client.json.post(url1, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data + + response = client.json.post(url2, json.dumps(data)) + assert response.status_code == 400, response.data + assert "us_id" in response.data + assert "bulk_tasks" in response.data def test_get_invalid_csv(client): From 7611785c3b0407d38c36033a95cd67305afc2890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Aug 2016 17:45:31 +0200 Subject: [PATCH 208/261] Add user_story_extra_info to task serializer --- taiga/projects/tasks/api.py | 1 + taiga/projects/tasks/serializers.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 9584a17b..5f9a06ae 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -79,6 +79,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, def get_queryset(self): qs = super().get_queryset() qs = qs.select_related("milestone", + "user_story", "project", "status", "owner", diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index b7232cf8..358b88ee 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -29,6 +29,17 @@ from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin +class UserStoryExtraInfoSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) + class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, @@ -54,6 +65,7 @@ class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, is_blocked = Field() blocked_note = Field() is_closed = MethodField() + user_story_extra_info = UserStoryExtraInfoSerializer(attr="user_story") def get_milestone_slug(self, obj): return obj.milestone.slug if obj.milestone else None From e03de182f3a3e077a19b2421b8a437b17c9d84e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Aug 2016 17:46:23 +0200 Subject: [PATCH 209/261] Fix an entrophy ploblem with custom attributes for epics in sample data --- taiga/projects/management/commands/sample_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index b659096a..9a4d2b22 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -521,7 +521,7 @@ class Command(BaseCommand): epic.save() custom_attributes_values = {str(ca.id): self.get_custom_attributes_value(ca.type) for ca - in project.epiccustomattributes.all() if self.sd.boolean()} + in project.epiccustomattributes.all().order_by("id") if self.sd.boolean()} if custom_attributes_values: epic.custom_attributes_values.attributes_values = custom_attributes_values epic.custom_attributes_values.save() From e68079b76911eb4fc1cb29947370da276518eec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 17 Aug 2016 12:00:00 +0200 Subject: [PATCH 210/261] Use new tags mixing in EpicSerializer --- taiga/projects/epics/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py index 29a94348..b02238cd 100644 --- a/taiga/projects/epics/serializers.py +++ b/taiga/projects/epics/serializers.py @@ -26,13 +26,14 @@ from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import WatchedResourceSerializer +from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, - serializers.LightSerializer): + TaggedInProjectResourceSerializer, serializers.LightSerializer): id = Field() ref = Field() @@ -48,7 +49,6 @@ class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, watchers = Field() is_blocked = Field() blocked_note = Field() - tags = Field() is_closed = MethodField() user_stories_counts = MethodField() From 94ea299c393c404662559fe6fb62df0cd2600fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 17 Aug 2016 14:21:07 +0200 Subject: [PATCH 211/261] Add epics info in user_story_extra_info in task serializer --- taiga/projects/tasks/api.py | 1 - taiga/projects/tasks/serializers.py | 14 +---------- taiga/projects/tasks/utils.py | 39 ++++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 5f9a06ae..9584a17b 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -79,7 +79,6 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, def get_queryset(self): qs = super().get_queryset() qs = qs.select_related("milestone", - "user_story", "project", "status", "owner", diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index 358b88ee..f0621581 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -29,18 +29,6 @@ from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.tagging.serializers import TaggedInProjectResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -class UserStoryExtraInfoSerializer(serializers.LightSerializer): - id = Field() - ref = Field() - subject = Field() - - def to_value(self, instance): - if instance is None: - return None - - return super().to_value(instance) - - class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, @@ -65,7 +53,7 @@ class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, is_blocked = Field() blocked_note = Field() is_closed = MethodField() - user_story_extra_info = UserStoryExtraInfoSerializer(attr="user_story") + user_story_extra_info = Field() def get_milestone_slug(self, obj): return obj.milestone.slug if obj.milestone else None diff --git a/taiga/projects/tasks/utils.py b/taiga/projects/tasks/utils.py index d10dddab..d5775d19 100644 --- a/taiga/projects/tasks/utils.py +++ b/taiga/projects/tasks/utils.py @@ -25,8 +25,44 @@ from taiga.projects.votes.utils import attach_total_voters_to_queryset from taiga.projects.votes.utils import attach_is_voter_to_queryset -def attach_extra_info(queryset, user=None, include_attachments=False): +def attach_user_story_extra_info(queryset, as_field="user_story_extra_info"): + """Attach userstory extra info as json column to each object of the queryset. + :param queryset: A Django user stories queryset object. + :param as_field: Attach the userstory extra info as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + + model = queryset.model + sql = """SELECT row_to_json(u) + FROM (SELECT "userstories_userstory"."id" AS "id", + "userstories_userstory"."ref" AS "ref", + "userstories_userstory"."subject" AS "subject", + (SELECT json_agg(row_to_json(t)) + FROM (SELECT "epics_epic"."id" AS "id", + "epics_epic"."ref" AS "ref", + "epics_epic"."subject" AS "subject", + "epics_epic"."color" AS "color", + json_build_object('id', "projects_project"."id", + 'name', "projects_project"."name", + 'slug', "projects_project"."slug") AS "project" + FROM "epics_relateduserstory" + INNER JOIN "epics_epic" + ON "epics_epic"."id" = "epics_relateduserstory"."epic_id" + INNER JOIN "projects_project" + ON "projects_project"."id" = "epics_epic"."project_id" + WHERE "epics_relateduserstory"."user_story_id" = "{tbl}"."user_story_id" + ORDER BY "projects_project"."name", "epics_epic"."ref") t) AS "epics" + FROM "userstories_userstory" + WHERE "userstories_userstory"."id" = "{tbl}"."user_story_id") u""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False): if include_attachments: queryset = attach_basic_attachments(queryset) queryset = queryset.extra(select={"include_attachments": "True"}) @@ -36,4 +72,5 @@ def attach_extra_info(queryset, user=None, include_attachments=False): queryset = attach_total_watchers_to_queryset(queryset) queryset = attach_is_voter_to_queryset(queryset, user) queryset = attach_is_watcher_to_queryset(queryset, user) + queryset = attach_user_story_extra_info(queryset) return queryset From f7c74fc63e52369361025e6767db4e7e2837ac2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 17 Aug 2016 15:40:52 +0200 Subject: [PATCH 212/261] Fix some tests --- taiga/projects/epics/api.py | 1 + .../test_epics_resources.py | 79 ------------------- .../test_projects_choices_resources.py | 4 +- 3 files changed, 3 insertions(+), 81 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 242b3ebe..cf7bed6e 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -118,6 +118,7 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, data.append({"epic_id": int(id), "order": order}) return services.update_epics_order_in_bulk(data, + "epics_order", project=obj.project) return {} diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index dff20c09..9183df1c 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -703,85 +703,6 @@ def test_bulk_create_related_userstories(client, data): assert results == [401, 403, 403, 451, 451] -def test_set_related_user_story(client, data): - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.public_epic.pk}) - edit_data = json.dumps({ - "us_id": data.public_us.pk, - "order": 33, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [401, 403, 403, 200, 200] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic1.pk}) - edit_data = json.dumps({ - "us_id": data.private_us1.pk, - "order": 33, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [401, 403, 403, 200, 200] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic2.pk}) - edit_data = json.dumps({ - "us_id": data.private_us2.pk, - "order": 33, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [404, 404, 404, 200, 200] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.blocked_epic.pk}) - edit_data = json.dumps({ - "us_id": data.blocked_us.pk, - "order": 33, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [404, 404, 404, 451, 451] - - -def test_unset_related_user_story(client, data): - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms - ] - - url = reverse('epics-unset-related-userstory', kwargs={"pk": data.public_epic.pk}) - edit_data = json.dumps({ - "us_id": data.public_related_us.user_story.pk, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [401, 403, 403, 200] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic1.pk}) - edit_data = json.dumps({ - "us_id": data.private_related_us1.user_story.pk, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [401, 403, 403, 200] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.private_epic2.pk}) - edit_data = json.dumps({ - "us_id": data.private_related_us2.user_story.pk, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [404, 404, 404, 200] - - url = reverse('epics-set-related-userstory', kwargs={"pk": data.blocked_epic.pk}) - edit_data = json.dumps({ - "us_id": data.blocked_related_us.user_story.pk, - }) - results = helper_test_http_method(client, 'post', url, edit_data, users) - assert results == [404, 404, 404, 451] - - def test_epic_action_upvote(client, data): public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) diff --git a/tests/integration/resources_permissions/test_projects_choices_resources.py b/tests/integration/resources_permissions/test_projects_choices_resources.py index 28e1f483..504b6a6f 100644 --- a/tests/integration/resources_permissions/test_projects_choices_resources.py +++ b/tests/integration/resources_permissions/test_projects_choices_resources.py @@ -2047,8 +2047,8 @@ def test_membership_action_bulk_create(client, data): bulk_data = { "project_id": data.blocked_project.id, "bulk_memberships": [ - {"role_id": data.private_membership2.role.pk, "email": "test1@test.com"}, - {"role_id": data.private_membership2.role.pk, "email": "test2@test.com"}, + {"role_id": data.blocked_membership.role.pk, "email": "test1@test.com"}, + {"role_id": data.blocked_membership.role.pk, "email": "test2@test.com"}, ] } bulk_data = json.dumps(bulk_data) From 275ce381f975869846214252da70d0d24b21f87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 17 Aug 2016 17:01:02 +0200 Subject: [PATCH 213/261] Fix tasks creation in bulk test --- taiga/projects/tasks/api.py | 29 ++++++++++--------- .../test_tasks_resources.py | 8 ++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 9584a17b..e722f935 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -251,24 +251,25 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): validator = validators.TasksBulkValidator(data=request.DATA) - if validator.is_valid(): - data = validator.data - project = Project.objects.get(id=data["project_id"]) - self.check_permissions(request, 'bulk_create', project) - if project.blocked_code is not None: - raise exc.Blocked(_("Blocked element")) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - tasks = services.create_tasks_in_bulk( - data["bulk_tasks"], milestone_id=data["milestone_id"], user_story_id=data["us_id"], - status_id=data.get("status_id") or project.default_task_status_id, - project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) + data = validator.data + project = Project.objects.get(id=data["project_id"]) + self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) - tasks = self.get_queryset().filter(id__in=[i.id for i in tasks]) - tasks_serialized = self.get_serializer_class()(tasks, many=True) + tasks = services.create_tasks_in_bulk( + data["bulk_tasks"], milestone_id=data["milestone_id"], user_story_id=data["us_id"], + status_id=data.get("status_id") or project.default_task_status_id, + project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) - return response.Ok(tasks_serialized.data) + tasks = self.get_queryset().filter(id__in=[i.id for i in tasks]) + tasks_serialized = self.get_serializer_class()(tasks, many=True) + + return response.Ok(tasks_serialized.data) - return response.BadRequest(validator.errors) def _bulk_update_order(self, order_field, request, **kwargs): validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA) diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 4d6427dd..d2fcc2e3 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -656,7 +656,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.public_task.user_story.pk, "project_id": data.public_task.project.pk, - "sprint_id": data.public_task.milestone.pk, + "milestone_id": data.public_task.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] @@ -665,7 +665,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.private_task1.user_story.pk, "project_id": data.private_task1.project.pk, - "sprint_id": data.private_task1.milestone.pk, + "milestone_id": data.private_task1.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] @@ -674,7 +674,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.private_task2.user_story.pk, "project_id": data.private_task2.project.pk, - "sprint_id": data.private_task2.milestone.pk, + "milestone_id": data.private_task2.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] @@ -683,7 +683,7 @@ def test_task_action_bulk_create(client, data): "bulk_tasks": "test1\ntest2", "us_id": data.blocked_task.user_story.pk, "project_id": data.blocked_task.project.pk, - "sprint_id": data.blocked_task.milestone.pk, + "milestone_id": data.blocked_task.milestone.pk, }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 451, 451] From 1cb924891285cbd75ee185db32bfcb83819c843a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 19 Aug 2016 09:59:41 +0200 Subject: [PATCH 214/261] Minor codee improvements --- taiga/permissions/choices.py | 2 +- taiga/projects/epics/api.py | 99 +++++++++++++++++------------------- 2 files changed, 49 insertions(+), 52 deletions(-) diff --git a/taiga/permissions/choices.py b/taiga/permissions/choices.py index 3564acc8..594d48ee 100644 --- a/taiga/permissions/choices.py +++ b/taiga/permissions/choices.py @@ -37,7 +37,7 @@ MEMBERS_PERMISSIONS = [ ('add_milestone', _('Add milestone')), ('modify_milestone', _('Modify milestone')), ('delete_milestone', _('Delete milestone')), - # US permissions + # Epic permissions ('view_epics', _('View epic')), ('add_epic', _('Add epic')), ('modify_epic', _('Modify epic')), diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index cf7bed6e..cc1b1c5d 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -111,16 +111,15 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, def _reorder_if_needed(self, obj, old_order_key, order_key): # Executes the extra ordering if there is a difference in the ordering keys - if old_order_key != order_key: - extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) - data = [{"epic_id": obj.id, "order": getattr(obj, "epics_order")}] - for id, order in extra_orders.items(): - data.append({"epic_id": int(id), "order": order}) + if old_order_key == order_key: + return {} - return services.update_epics_order_in_bulk(data, - "epics_order", - project=obj.project) - return {} + extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) + data = [{"epic_id": obj.id, "order": getattr(obj, "epics_order")}] + for id, order in extra_orders.items(): + data.append({"epic_id": int(id), "order": order}) + + return services.update_epics_order_in_bulk(data, "epics_order", project=obj.project) def post_save(self, obj, created=False): if not created: @@ -205,26 +204,26 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): validator = validators.EpicsBulkValidator(data=request.DATA) - if validator.is_valid(): - data = validator.data - project = Project.objects.get(id=data["project_id"]) - self.check_permissions(request, "bulk_create", project) - if project.blocked_code is not None: - raise exc.Blocked(_("Blocked element")) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - epics = services.create_epics_in_bulk( - data["bulk_epics"], - status_id=data.get("status_id") or project.default_epic_status_id, - project=project, - owner=request.user, - callback=self.post_save, precall=self.pre_save) + data = validator.data + project = Project.objects.get(id=data["project_id"]) + self.check_permissions(request, "bulk_create", project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) - epics = self.get_queryset().filter(id__in=[i.id for i in epics]) - epics_serialized = self.get_serializer_class()(epics, many=True) + epics = services.create_epics_in_bulk( + data["bulk_epics"], + status_id=data.get("status_id") or project.default_epic_status_id, + project=project, + owner=request.user, + callback=self.post_save, precall=self.pre_save) - return response.Ok(epics_serialized.data) + epics = self.get_queryset().filter(id__in=[i.id for i in epics]) + epics_serialized = self.get_serializer_class()(epics, many=True) - return response.BadRequest(validator.errors) + return response.Ok(epics_serialized.data) class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, ModelCrudViewSet): @@ -254,18 +253,16 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod def _reorder_if_needed(self, obj, old_order_key, order_key): # Executes the extra ordering if there is a difference in the ordering keys - if old_order_key != order_key: - extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) - data = [{"us_id": obj.id, "order": getattr(obj, "order")}] - for id, order in extra_orders.items(): - data.append({"epic_id": int(id), "order": order}) + if old_order_key == order_key: + return {} - return services.update_epic_related_userstories_order_in_bulk( - data, - epic=obj.epic - ) + extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) + data = [{"us_id": obj.id, "order": getattr(obj, "order")}] + for id, order in extra_orders.items(): + data.append({"epic_id": int(id), "order": order}) + + return services.update_epic_related_userstories_order_in_bulk(data, epic=obj.epic) - return {} def post_save(self, obj, created=False): if not created: @@ -280,27 +277,27 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): validator = validators.CrateRelatedUserStoriesBulkValidator(data=request.DATA) - if validator.is_valid(): - data = validator.data + if not validator.is_valid(): + return response.BadRequest(validator.errors) - epic = get_object_or_404(models.Epic, id=kwargs["epic"]) - project = epic.project + data = validator.data - self.check_permissions(request, 'bulk_create', project) - if project.blocked_code is not None: - raise exc.Blocked(_("Blocked element")) + epic = get_object_or_404(models.Epic, id=kwargs["epic"]) + project = epic.project - services.create_related_userstories_in_bulk( - data["userstories"], - epic, - project=project, - owner=request.user - ) + self.check_permissions(request, 'bulk_create', project) + if project.blocked_code is not None: + raise exc.Blocked(_("Blocked element")) - related_uss_serialized = self.get_serializer_class()(epic.relateduserstory_set.all(), many=True) - return response.Ok(related_uss_serialized.data) + services.create_related_userstories_in_bulk( + data["userstories"], + epic, + project=project, + owner=request.user + ) - return response.BadRequest(validator.errors) + related_uss_serialized = self.get_serializer_class()(epic.relateduserstory_set.all(), many=True) + return response.Ok(related_uss_serialized.data) class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet): From 8d0c9ee1c1a89aff35bd0ab19d45d93675246f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 22 Aug 2016 13:05:44 +0200 Subject: [PATCH 215/261] Fix test and RelatedUserStoyr API nested endpoint --- taiga/base/api/mixins.py | 29 ++++++++++++++++++++++++---- taiga/base/utils/db.py | 6 +++--- taiga/permissions/services.py | 1 - taiga/projects/epics/api.py | 8 ++++---- taiga/projects/epics/serializers.py | 1 + taiga/projects/epics/services.py | 30 ++++++++++++++++++++++++++++- taiga/projects/epics/validators.py | 2 +- tests/integration/test_epics.py | 19 ++++++++---------- 8 files changed, 71 insertions(+), 25 deletions(-) diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index c38b5cb7..b01d7cf2 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -211,14 +211,14 @@ class UpdateModelMixin: Set any attributes on the object that are implicit in the request. """ # pk and/or slug attributes are implicit in the URL. - lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field - lookup = self.kwargs.get(lookup_url_kwarg, None) + ##lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + ##lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) slug_field = slug and self.slug_field or None - if lookup: - setattr(obj, self.lookup_field, lookup) + ##if lookup: + ## setattr(obj, self.lookup_field, lookup) if pk: setattr(obj, 'pk', pk) @@ -253,6 +253,27 @@ class DestroyModelMixin: return response.NoContent() +class NestedViewSetMixin(object): + def get_queryset(self): + return self._filter_queryset_by_parents_lookups(super().get_queryset()) + + def _filter_queryset_by_parents_lookups(self, queryset): + parents_query_dict = self._get_parents_query_dict() + if parents_query_dict: + return queryset.filter(**parents_query_dict) + else: + return queryset + + def _get_parents_query_dict(self): + result = {} + for kwarg_name in self.kwargs: + query_value = self.kwargs.get(kwarg_name) + result[kwarg_name] = query_value + return result + + +## TODO: Move blocked mixind out of the base module because is related to project + class BlockeableModelMixin: def is_blocked(self, obj): raise NotImplementedError("is_blocked must be overridden") diff --git a/taiga/base/utils/db.py b/taiga/base/utils/db.py index a6c21d6b..eb610fff 100644 --- a/taiga/base/utils/db.py +++ b/taiga/base/utils/db.py @@ -132,13 +132,13 @@ def update_attr_in_bulk_for_ids(values, attr, model): """ values = [str((id, order)) for id, order in values.items()] sql = """ - UPDATE {tbl} - SET {attr}=update_values.column2 + UPDATE "{tbl}" + SET "{attr}"=update_values.column2 FROM ( VALUES {values} ) AS update_values - WHERE {tbl}.id=update_values.column1; + WHERE "{tbl}"."id"=update_values.column1; """.format(tbl=model._meta.db_table, values=', '.join(values), attr=attr) diff --git a/taiga/permissions/services.py b/taiga/permissions/services.py index 6d89c168..50d8d72d 100644 --- a/taiga/permissions/services.py +++ b/taiga/permissions/services.py @@ -78,7 +78,6 @@ def user_has_perm(user, perm, obj=None, cache="user"): in cache """ project = _get_object_project(obj) - if not project: return False diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index cc1b1c5d..5888865d 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -232,7 +232,7 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod validator_class = validators.EpicRelatedUserStoryValidator model = models.RelatedUserStory permission_classes = (permissions.EpicRelatedUserStoryPermission,) - lookup_field = "user_story_id" + lookup_field = "user_story" """ Updating the order attribute can affect the ordering of another userstories in the epic @@ -259,7 +259,7 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) data = [{"us_id": obj.id, "order": getattr(obj, "order")}] for id, order in extra_orders.items(): - data.append({"epic_id": int(id), "order": order}) + data.append({"us_id": int(id), "order": order}) return services.update_epic_related_userstories_order_in_bulk(data, epic=obj.epic) @@ -268,8 +268,8 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod if not created: # Let's reorder the related stuff after edit the element orders_updated = self._reorder_if_needed(obj, - self._old_epics_order_key, - self._epics_order_key(obj)) + self._old_order_key, + self._order_key(obj)) self.headers["Taiga-Info-Order-Updated"] = json.dumps(orders_updated) super().post_save(obj, created) diff --git a/taiga/projects/epics/serializers.py b/taiga/projects/epics/serializers.py index b02238cd..339272de 100644 --- a/taiga/projects/epics/serializers.py +++ b/taiga/projects/epics/serializers.py @@ -81,5 +81,6 @@ class EpicNeighborsSerializer(NeighborsSerializerMixin, EpicSerializer): class EpicRelatedUserStorySerializer(serializers.LightSerializer): + epic = Field(attr="epic_id") user_story = Field(attr="user_story_id") order = Field() diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index 145e2339..f953a574 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -26,9 +26,9 @@ from django.db import connection from django.utils.translation import ugettext as _ from taiga.base.utils import db, text -from taiga.projects.services import apply_order_updates from taiga.projects.epics.apps import connect_epics_signals from taiga.projects.epics.apps import disconnect_epics_signals +from taiga.projects.services import apply_order_updates from taiga.projects.userstories.apps import connect_userstories_signals from taiga.projects.userstories.apps import disconnect_userstories_signals from taiga.projects.userstories.services import get_userstories_from_bulk @@ -127,6 +127,34 @@ def create_related_userstories_in_bulk(bulk_data, epic, **additional_fields): return userstories +def update_epic_related_userstories_order_in_bulk(bulk_data: list, epic: object): + """ + Updates the order of the related userstories of an specific epic. + `bulk_data` should be a list of dicts with the following format: + `epic` is the epic with related stories. + + [{'us_id': , 'order': }, ...] + """ + related_user_stories = epic.relateduserstory_set.all() + rus_orders = {rus.id: rus.order for rus in related_user_stories} + + rus_conversion = {rus.user_story_id: rus.id for rus in related_user_stories} + new_rus_orders = {rus_conversion[e["us_id"]]: e["order"] for e in bulk_data + if e["us_id"] in rus_conversion} + + apply_order_updates(rus_orders, new_rus_orders) + + if rus_orders: + related_user_story_ids = rus_orders.keys() + events.emit_event_for_ids(ids=related_user_story_ids, + content_type="epics.relateduserstory", + projectid=epic.project_id) + + db.update_attr_in_bulk_for_ids(rus_orders, "order", models.RelatedUserStory) + + return rus_orders + + ##################################################### # CSV ##################################################### diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 88214aa6..175f1c3c 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -63,4 +63,4 @@ class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsVal class EpicRelatedUserStoryValidator(validators.ModelValidator): class Meta: model = models.RelatedUserStory - read_only_fields = ('id', 'epic', 'user_story') + read_only_fields = ('id',) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 5d4657df..0ca87d09 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -119,13 +119,13 @@ def test_set_related_userstory(client): url = reverse('epics-related-userstories-list', args=[epic.pk]) data = { - "user_story": us.id + "user_story": us.id, + "epic": epic.pk } client.login(user) response = client.json.post(url, json.dumps(data)) - print(response.data) - assert response.status_code == 200 - assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} + + assert response.status_code == 201 def test_set_related_userstory_existing(client): @@ -136,18 +136,15 @@ def test_set_related_userstory_existing(client): f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) f.MembershipFactory.create(project=us.project, user=user, is_admin=True) - url = reverse('epics-related-userstories-list', args=[epic.pk]) - + url = reverse('epics-related-userstories-detail', args=[epic.pk, us.pk]) data = { - "user_story": us.id, "order": 77 } client.login(user) - response = client.json.post(url, json.dumps(data)) + response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 - assert response.data['user_stories_counts'] == {'opened': 1, 'closed': 0} - related_us = models.RelatedUserStory.objects.get(id=related_us.id) + related_us.refresh_from_db() assert related_us.order == 77 @@ -158,7 +155,7 @@ def test_unset_related_userstory(client): related_us = f.RelatedUserStory.create(epic=epic, user_story=us, order=55) f.MembershipFactory.create(project=epic.project, user=user, is_admin=True) - url = reverse('epics-related-userstories-detail', args=[epic.pk, us.pk]) + url = reverse('epics-related-userstories-detail', args=[epic.pk, us.id]) client.login(user) response = client.delete(url) From 5e1ca747d38f9c96cf1d27fdef181fb69b8054fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 24 Aug 2016 18:27:47 +0200 Subject: [PATCH 216/261] [i18n] Update locales --- taiga/locale/en/LC_MESSAGES/django.po | 1812 ++++++++++++++----------- 1 file changed, 1031 insertions(+), 781 deletions(-) diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index 4cde7d23..627ffe46 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: 2016-05-01 19:09+0200\n" +"POT-Creation-Date: 2016-08-24 18:09+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -16,339 +16,340 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "" -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "" + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "" + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "" + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "" + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "" + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" 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:94 -msgid "Token not matches any valid invitation." -msgstr "" - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "" - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "" - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "" -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "" -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "" -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "" -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "" -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "" -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "" -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "" -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "" -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "" -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "" -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "" -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "" -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr "" -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:212 taiga/projects/epics/api.py:288 +#: taiga/projects/issues/api.py:235 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:258 taiga/projects/tasks/api.py:281 +#: taiga/projects/userstories/api.py:332 taiga/projects/userstories/api.py:381 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "" -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "" -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "" -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "" -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "" -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "" -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "" -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "" -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "" -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "" -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "" -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "" -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "" -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "" -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "" -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "" -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "" -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "" -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "" -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:79 taiga/base/filters.py:460 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:133 taiga/base/filters.py:240 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "" -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "" @@ -403,7 +404,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -457,103 +458,88 @@ msgid "" " " msgstr "" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "" - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -573,15 +559,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -732,77 +718,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "" + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:510 taiga/projects/models.py:543 +#: taiga/projects/models.py:579 taiga/projects/models.py:601 +#: taiga/projects/models.py:635 taiga/projects/models.py:655 +#: taiga/projects/models.py:675 taiga/projects/models.py:707 +#: taiga/projects/models.py:727 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:731 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:735 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "" @@ -825,7 +831,7 @@ msgid "" msgstr "" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:103 taiga/users/admin.py:120 msgid "Extra info" msgstr "" @@ -851,504 +857,572 @@ msgid "" "[Taiga] Feedback from %(full_name)s <%(email)s>\n" msgstr "" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:151 +#: taiga/projects/issues/api.py:135 taiga/projects/tasks/api.py:197 +#: taiga/projects/userstories/api.py:265 msgid "The project doesn't exist" msgstr "" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: 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:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:65 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." +#: taiga/hooks/event_hooks.py:70 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:83 msgid "Invalid issue comment information" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:102 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." +msgstr "" + +#: taiga/hooks/event_hooks.py:106 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:119 +msgid "Invalid issue information" +msgstr "" + +#: taiga/hooks/event_hooks.py:148 taiga/hooks/event_hooks.py:170 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:155 +#, python-brace-format +msgid "" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:160 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"Changed status from {platform} commit.\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:178 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:183 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." +#: taiga/hooks/event_hooks.py:202 +msgid "The referenced element doesn't exist" msgstr "" -#: taiga/hooks/github/event_hooks.py:201 -#, python-brace-format -msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:218 +msgid "The status doesn't exist" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -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/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" 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:75 -msgid "Add wiki page" -msgstr "" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:97 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:109 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:117 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:123 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:128 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:142 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:195 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:196 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:210 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:211 +msgid "Make private" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "" -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:498 +#: taiga/projects/models.py:520 taiga/projects/models.py:557 +#: taiga/projects/models.py:585 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:738 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:581 taiga/projects/models.py:605 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:679 taiga/projects/models.py:709 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 -msgid "Text" +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:29 -msgid "Date" +msgid "Multi-Line Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:30 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "" -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:91 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:518 +#: taiga/projects/models.py:553 taiga/projects/models.py:609 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:683 taiga/projects/models.py:711 +#: taiga/users/models.py:139 +msgid "color" +msgstr "" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "" @@ -1404,7 +1478,7 @@ msgstr "" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "" @@ -1451,95 +1525,75 @@ msgstr "" msgid "To:" msgstr "" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:153 msgid "You don't have permissions to set this sprint to this issue." msgstr "" -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:157 msgid "You don't have permissions to set this status to this issue." msgstr "" -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:161 msgid "You don't have permissions to set this severity to this issue." msgstr "" -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:165 msgid "You don't have permissions to set this priority to this issue." msgstr "" -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:169 msgid "You don't have permissions to set this type to this issue." msgstr "" -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:603 taiga/projects/models.py:677 +#: taiga/projects/models.py:729 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "" @@ -1551,8 +1605,9 @@ msgstr "" msgid "estimated finish date" msgstr "" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:516 +#: taiga/projects/models.py:549 taiga/projects/models.py:607 +#: taiga/projects/models.py:681 msgid "is closed" msgstr "" @@ -1564,120 +1619,132 @@ msgstr "" 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" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:733 msgid "user order" msgstr "" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "" -#: taiga/projects/models.py:116 -msgid "default points" +#: taiga/projects/models.py:111 +msgid "default epic status" msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:744 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:746 msgid "active backlog panel" msgstr "" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:748 msgid "active kanban panel" msgstr "" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:750 msgid "active wiki panel" msgstr "" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:752 msgid "active issues panel" msgstr "" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:755 msgid "videoconference system" msgstr "" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:757 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "" @@ -1685,169 +1752,251 @@ msgstr "" msgid "user permissions" msgstr "" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:499 msgid "modules config" msgstr "" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:551 msgid "is archived" msgstr "" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:555 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:583 taiga/userstorage/models.py:33 msgid "value" msgstr "" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:741 msgid "default owner's role" msgstr "" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:759 msgid "default options" msgstr "" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:760 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:761 msgid "us statuses" msgstr "" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:762 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:763 msgid "task statuses" msgstr "" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:764 msgid "issue statuses" msgstr "" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:765 msgid "issue types" msgstr "" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:766 msgid "priorities" msgstr "" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:767 msgid "severities" msgstr "" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:768 msgid "roles" msgstr "" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:425 msgid "Invalid value for notify level" msgstr "" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2319,141 +2468,135 @@ msgid "" "[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" msgstr "" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." -msgstr "" - -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:581 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:584 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:588 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:591 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:94 taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:97 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2469,8 +2612,34 @@ msgstr "" msgid "is iocaine" msgstr "" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 @@ -2812,12 +2981,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -2828,12 +2997,12 @@ msgid "" msgstr "" #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -2842,303 +3011,388 @@ msgid "" msgstr "" #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:116 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:120 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:210 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:217 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:232 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:293 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:296 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" msgstr "" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" msgstr "" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3166,145 +3420,137 @@ msgstr "" msgid "Important dates" msgstr "" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "" - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "" @@ -3425,47 +3671,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" +#: taiga/users/validators.py:45 +msgid "invalid" msgstr "" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "" + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "" From 7313dfd9965a0c04cadb8cbd32161bbac2499ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 24 Aug 2016 18:29:36 +0200 Subject: [PATCH 217/261] Add comment permissions to default project templates --- .../fixtures/initial_project_templates.json | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index 5c711c00..9bc6d969 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -8,7 +8,7 @@ "description": "The agile product backlog in Scrum is a prioritized features list, containing short descriptions of all functionality desired in the product. When applying Scrum, it's not necessary to start a project with a lengthy, upfront effort to document all requirements. The Scrum product backlog is then allowed to grow and change as more is learned about the product and its customers", "order": 1, "created_date": "2014-04-22T14:48:43.596Z", - "modified_date": "2016-07-07T13:18:25.350Z", + "modified_date": "2016-08-24T16:26:40.845Z", "default_owner_role": "product-owner", "is_epics_activated": true, "is_backlog_activated": true, @@ -17,16 +17,16 @@ "is_issues_activated": true, "videoconferences": null, "videoconferences_extra_data": "", - "default_options": "{\"issue_status\": \"New\", \"task_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"points\": \"?\", \"priority\": \"Normal\", \"us_status\": \"New\", \"epic_status\": \"New\"}", - "epic_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Ready\", \"is_closed\": false, \"slug\": \"ready\", \"order\": 2, \"color\": \"#ff8a84\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#ff9900\"}, {\"name\": \"Ready for test\", \"is_closed\": false, \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#fcc000\"}, {\"name\": \"Done\", \"is_closed\": true, \"slug\": \"done\", \"order\": 5, \"color\": \"#669900\"}]", - "us_statuses": "[{\"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"order\": 1}, {\"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#ff8a84\", \"order\": 2}, {\"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#ff9900\", \"order\": 3}, {\"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#fcc000\", \"order\": 4}, {\"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\", \"is_closed\": true, \"is_archived\": false, \"color\": \"#669900\", \"order\": 5}, {\"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\", \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"order\": 6}]", - "points": "[{\"name\": \"?\", \"value\": null, \"order\": 1}, {\"name\": \"0\", \"value\": 0.0, \"order\": 2}, {\"name\": \"1/2\", \"value\": 0.5, \"order\": 3}, {\"name\": \"1\", \"value\": 1.0, \"order\": 4}, {\"name\": \"2\", \"value\": 2.0, \"order\": 5}, {\"name\": \"3\", \"value\": 3.0, \"order\": 6}, {\"name\": \"5\", \"value\": 5.0, \"order\": 7}, {\"name\": \"8\", \"value\": 8.0, \"order\": 8}, {\"name\": \"10\", \"value\": 10.0, \"order\": 9}, {\"name\": \"13\", \"value\": 13.0, \"order\": 10}, {\"name\": \"20\", \"value\": 20.0, \"order\": 11}, {\"name\": \"40\", \"value\": 40.0, \"order\": 12}]", - "task_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#ff9900\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#ffcc00\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#669900\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#999999\"}]", - "issue_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#8C2318\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#5E8C6A\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#88A65E\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#BFB35A\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#89BAB4\"}, {\"name\": \"Rejected\", \"is_closed\": true, \"slug\": \"rejected\", \"order\": 6, \"color\": \"#CC0000\"}, {\"name\": \"Postponed\", \"is_closed\": false, \"slug\": \"posponed\", \"order\": 7, \"color\": \"#666666\"}]", - "issue_types": "[{\"name\": \"Bug\", \"order\": 1, \"color\": \"#89BAB4\"}, {\"name\": \"Question\", \"order\": 2, \"color\": \"#ba89a8\"}, {\"name\": \"Enhancement\", \"order\": 3, \"color\": \"#89a8ba\"}]", - "priorities": "[{\"name\": \"Low\", \"order\": 1, \"color\": \"#666666\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#669933\"}, {\"name\": \"High\", \"order\": 5, \"color\": \"#CC0000\"}]", - "severities": "[{\"name\": \"Wishlist\", \"order\": 1, \"color\": \"#666666\"}, {\"name\": \"Minor\", \"order\": 2, \"color\": \"#669933\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#0000FF\"}, {\"name\": \"Important\", \"order\": 4, \"color\": \"#FFA500\"}, {\"name\": \"Critical\", \"order\": 5, \"color\": \"#CC0000\"}]", - "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\"], \"order\": 60}]" + "default_options": "{\"epic_status\": \"New\", \"issue_status\": \"New\", \"task_status\": \"New\", \"points\": \"?\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"priority\": \"Normal\", \"us_status\": \"New\"}", + "epic_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"order\": 2}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"order\": 3}, {\"is_closed\": false, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"order\": 4}, {\"is_closed\": true, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"order\": 5}]", + "us_statuses": "[{\"is_archived\": false, \"name\": \"New\", \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready\", \"slug\": \"ready\", \"order\": 2, \"color\": \"#ff8a84\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"In progress\", \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#ff9900\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#fcc000\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Done\", \"slug\": \"done\", \"order\": 5, \"color\": \"#669900\", \"wip_limit\": null, \"is_closed\": true}, {\"is_archived\": true, \"name\": \"Archived\", \"slug\": \"archived\", \"order\": 6, \"color\": \"#5c3566\", \"wip_limit\": null, \"is_closed\": true}]", + "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]", + "task_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#ffcc00\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#669900\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#999999\", \"slug\": \"needs-info\", \"order\": 5}]", + "issue_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#8C2318\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#5E8C6A\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#88A65E\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#BFB35A\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#89BAB4\", \"slug\": \"needs-info\", \"order\": 5}, {\"is_closed\": true, \"name\": \"Rejected\", \"color\": \"#CC0000\", \"slug\": \"rejected\", \"order\": 6}, {\"is_closed\": false, \"name\": \"Postponed\", \"color\": \"#666666\", \"slug\": \"posponed\", \"order\": 7}]", + "issue_types": "[{\"name\": \"Bug\", \"color\": \"#89BAB4\", \"order\": 1}, {\"name\": \"Question\", \"color\": \"#ba89a8\", \"order\": 2}, {\"name\": \"Enhancement\", \"color\": \"#89a8ba\", \"order\": 3}]", + "priorities": "[{\"name\": \"Low\", \"color\": \"#666666\", \"order\": 1}, {\"name\": \"Normal\", \"color\": \"#669933\", \"order\": 3}, {\"name\": \"High\", \"color\": \"#CC0000\", \"order\": 5}]", + "severities": "[{\"name\": \"Wishlist\", \"color\": \"#666666\", \"order\": 1}, {\"name\": \"Minor\", \"color\": \"#669933\", \"order\": 2}, {\"name\": \"Normal\", \"color\": \"#0000FF\", \"order\": 3}, {\"name\": \"Important\", \"color\": \"#FFA500\", \"order\": 4}, {\"name\": \"Critical\", \"color\": \"#CC0000\", \"order\": 5}]", + "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 60}]" } }, { @@ -38,7 +38,7 @@ "description": "Kanban is a method for managing knowledge work with an emphasis on just-in-time delivery while not overloading the team members. In this approach, the process, from definition of a task to its delivery to the customer, is displayed for participants to see and team members pull work from a queue.", "order": 2, "created_date": "2014-04-22T14:50:19.738Z", - "modified_date": "2016-07-07T13:18:28.186Z", + "modified_date": "2016-08-24T16:26:45.365Z", "default_owner_role": "product-owner", "is_epics_activated": true, "is_backlog_activated": false, @@ -47,16 +47,16 @@ "is_issues_activated": false, "videoconferences": null, "videoconferences_extra_data": "", - "default_options": "{\"issue_status\": \"New\", \"task_status\": \"New\", \"severity\": \"Normal\", \"issue_type\": \"Bug\", \"points\": \"?\", \"priority\": \"Normal\", \"us_status\": \"New\", \"epic_status\": \"New\"}", - "epic_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Ready\", \"is_closed\": false, \"slug\": \"ready\", \"order\": 2, \"color\": \"#ff8a84\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#ff9900\"}, {\"name\": \"Ready for test\", \"is_closed\": false, \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#fcc000\"}, {\"name\": \"Done\", \"is_closed\": true, \"slug\": \"done\", \"order\": 5, \"color\": \"#669900\"}]", - "us_statuses": "[{\"wip_limit\": null, \"name\": \"New\", \"slug\": \"new\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#999999\", \"order\": 1}, {\"wip_limit\": null, \"name\": \"Ready\", \"slug\": \"ready\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#f57900\", \"order\": 2}, {\"wip_limit\": null, \"name\": \"In progress\", \"slug\": \"in-progress\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#729fcf\", \"order\": 3}, {\"wip_limit\": null, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"is_closed\": false, \"is_archived\": false, \"color\": \"#4e9a06\", \"order\": 4}, {\"wip_limit\": null, \"name\": \"Done\", \"slug\": \"done\", \"is_closed\": true, \"is_archived\": false, \"color\": \"#cc0000\", \"order\": 5}, {\"wip_limit\": null, \"name\": \"Archived\", \"slug\": \"archived\", \"is_closed\": true, \"is_archived\": true, \"color\": \"#5c3566\", \"order\": 6}]", - "points": "[{\"name\": \"?\", \"value\": null, \"order\": 1}, {\"name\": \"0\", \"value\": 0.0, \"order\": 2}, {\"name\": \"1/2\", \"value\": 0.5, \"order\": 3}, {\"name\": \"1\", \"value\": 1.0, \"order\": 4}, {\"name\": \"2\", \"value\": 2.0, \"order\": 5}, {\"name\": \"3\", \"value\": 3.0, \"order\": 6}, {\"name\": \"5\", \"value\": 5.0, \"order\": 7}, {\"name\": \"8\", \"value\": 8.0, \"order\": 8}, {\"name\": \"10\", \"value\": 10.0, \"order\": 9}, {\"name\": \"13\", \"value\": 13.0, \"order\": 10}, {\"name\": \"20\", \"value\": 20.0, \"order\": 11}, {\"name\": \"40\", \"value\": 40.0, \"order\": 12}]", - "task_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#f57900\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#4e9a06\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#cc0000\"}]", - "issue_statuses": "[{\"name\": \"New\", \"is_closed\": false, \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"In progress\", \"is_closed\": false, \"slug\": \"in-progress\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Ready for test\", \"is_closed\": true, \"slug\": \"ready-for-test\", \"order\": 3, \"color\": \"#f57900\"}, {\"name\": \"Closed\", \"is_closed\": true, \"slug\": \"closed\", \"order\": 4, \"color\": \"#4e9a06\"}, {\"name\": \"Needs Info\", \"is_closed\": false, \"slug\": \"needs-info\", \"order\": 5, \"color\": \"#cc0000\"}, {\"name\": \"Rejected\", \"is_closed\": true, \"slug\": \"rejected\", \"order\": 6, \"color\": \"#d3d7cf\"}, {\"name\": \"Postponed\", \"is_closed\": false, \"slug\": \"posponed\", \"order\": 7, \"color\": \"#75507b\"}]", - "issue_types": "[{\"name\": \"Bug\", \"order\": 1, \"color\": \"#cc0000\"}, {\"name\": \"Question\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Enhancement\", \"order\": 3, \"color\": \"#4e9a06\"}]", - "priorities": "[{\"name\": \"Low\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#4e9a06\"}, {\"name\": \"High\", \"order\": 5, \"color\": \"#CC0000\"}]", - "severities": "[{\"name\": \"Wishlist\", \"order\": 1, \"color\": \"#999999\"}, {\"name\": \"Minor\", \"order\": 2, \"color\": \"#729fcf\"}, {\"name\": \"Normal\", \"order\": 3, \"color\": \"#4e9a06\"}, {\"name\": \"Important\", \"order\": 4, \"color\": \"#f57900\"}, {\"name\": \"Critical\", \"order\": 5, \"color\": \"#CC0000\"}]", - "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\"], \"order\": 60}]" + "default_options": "{\"epic_status\": \"New\", \"issue_status\": \"New\", \"task_status\": \"New\", \"points\": \"?\", \"issue_type\": \"Bug\", \"severity\": \"Normal\", \"priority\": \"Normal\", \"us_status\": \"New\"}", + "epic_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"Ready\", \"color\": \"#ff8a84\", \"slug\": \"ready\", \"order\": 2}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#ff9900\", \"slug\": \"in-progress\", \"order\": 3}, {\"is_closed\": false, \"name\": \"Ready for test\", \"color\": \"#fcc000\", \"slug\": \"ready-for-test\", \"order\": 4}, {\"is_closed\": true, \"name\": \"Done\", \"color\": \"#669900\", \"slug\": \"done\", \"order\": 5}]", + "us_statuses": "[{\"is_archived\": false, \"name\": \"New\", \"slug\": \"new\", \"order\": 1, \"color\": \"#999999\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready\", \"slug\": \"ready\", \"order\": 2, \"color\": \"#f57900\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"In progress\", \"slug\": \"in-progress\", \"order\": 3, \"color\": \"#729fcf\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Ready for test\", \"slug\": \"ready-for-test\", \"order\": 4, \"color\": \"#4e9a06\", \"wip_limit\": null, \"is_closed\": false}, {\"is_archived\": false, \"name\": \"Done\", \"slug\": \"done\", \"order\": 5, \"color\": \"#cc0000\", \"wip_limit\": null, \"is_closed\": true}, {\"is_archived\": true, \"name\": \"Archived\", \"slug\": \"archived\", \"order\": 6, \"color\": \"#5c3566\", \"wip_limit\": null, \"is_closed\": true}]", + "points": "[{\"value\": null, \"name\": \"?\", \"order\": 1}, {\"value\": 0.0, \"name\": \"0\", \"order\": 2}, {\"value\": 0.5, \"name\": \"1/2\", \"order\": 3}, {\"value\": 1.0, \"name\": \"1\", \"order\": 4}, {\"value\": 2.0, \"name\": \"2\", \"order\": 5}, {\"value\": 3.0, \"name\": \"3\", \"order\": 6}, {\"value\": 5.0, \"name\": \"5\", \"order\": 7}, {\"value\": 8.0, \"name\": \"8\", \"order\": 8}, {\"value\": 10.0, \"name\": \"10\", \"order\": 9}, {\"value\": 13.0, \"name\": \"13\", \"order\": 10}, {\"value\": 20.0, \"name\": \"20\", \"order\": 11}, {\"value\": 40.0, \"name\": \"40\", \"order\": 12}]", + "task_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"order\": 5}]", + "issue_statuses": "[{\"is_closed\": false, \"name\": \"New\", \"color\": \"#999999\", \"slug\": \"new\", \"order\": 1}, {\"is_closed\": false, \"name\": \"In progress\", \"color\": \"#729fcf\", \"slug\": \"in-progress\", \"order\": 2}, {\"is_closed\": true, \"name\": \"Ready for test\", \"color\": \"#f57900\", \"slug\": \"ready-for-test\", \"order\": 3}, {\"is_closed\": true, \"name\": \"Closed\", \"color\": \"#4e9a06\", \"slug\": \"closed\", \"order\": 4}, {\"is_closed\": false, \"name\": \"Needs Info\", \"color\": \"#cc0000\", \"slug\": \"needs-info\", \"order\": 5}, {\"is_closed\": true, \"name\": \"Rejected\", \"color\": \"#d3d7cf\", \"slug\": \"rejected\", \"order\": 6}, {\"is_closed\": false, \"name\": \"Postponed\", \"color\": \"#75507b\", \"slug\": \"posponed\", \"order\": 7}]", + "issue_types": "[{\"name\": \"Bug\", \"color\": \"#cc0000\", \"order\": 1}, {\"name\": \"Question\", \"color\": \"#729fcf\", \"order\": 2}, {\"name\": \"Enhancement\", \"color\": \"#4e9a06\", \"order\": 3}]", + "priorities": "[{\"name\": \"Low\", \"color\": \"#999999\", \"order\": 1}, {\"name\": \"Normal\", \"color\": \"#4e9a06\", \"order\": 3}, {\"name\": \"High\", \"color\": \"#CC0000\", \"order\": 5}]", + "severities": "[{\"name\": \"Wishlist\", \"color\": \"#999999\", \"order\": 1}, {\"name\": \"Minor\", \"color\": \"#729fcf\", \"order\": 2}, {\"name\": \"Normal\", \"color\": \"#4e9a06\", \"order\": 3}, {\"name\": \"Important\", \"color\": \"#f57900\", \"order\": 4}, {\"name\": \"Critical\", \"color\": \"#CC0000\", \"order\": 5}]", + "roles": "[{\"name\": \"UX\", \"computable\": true, \"slug\": \"ux\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 10}, {\"name\": \"Design\", \"computable\": true, \"slug\": \"design\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 20}, {\"name\": \"Front\", \"computable\": true, \"slug\": \"front\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 30}, {\"name\": \"Back\", \"computable\": true, \"slug\": \"back\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 40}, {\"name\": \"Product Owner\", \"computable\": false, \"slug\": \"product-owner\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"add_milestone\", \"modify_milestone\", \"delete_milestone\", \"view_milestones\", \"view_project\", \"add_task\", \"modify_task\", \"delete_task\", \"view_tasks\", \"add_us\", \"modify_us\", \"delete_us\", \"view_us\", \"add_wiki_page\", \"modify_wiki_page\", \"delete_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"add_epic\", \"modify_epic\", \"delete_epic\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 50}, {\"name\": \"Stakeholder\", \"computable\": false, \"slug\": \"stakeholder\", \"permissions\": [\"add_issue\", \"modify_issue\", \"delete_issue\", \"view_issues\", \"view_milestones\", \"view_project\", \"view_tasks\", \"view_us\", \"modify_wiki_page\", \"view_wiki_pages\", \"add_wiki_link\", \"delete_wiki_link\", \"view_wiki_links\", \"view_epics\", \"comment_epic\", \"comment_us\", \"comment_task\", \"comment_issue\", \"comment_wiki_page\"], \"order\": 60}]" } } ] From 544cdc00d086041f2f328a9f6a143060dcba63fd Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 25 Aug 2016 10:08:27 +0200 Subject: [PATCH 218/261] Adding permission tests for epic related user stories --- taiga/projects/epics/services.py | 9 ++++-- .../test_epics_resources.py | 28 ------------------- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index f953a574..bf0edff5 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -136,11 +136,12 @@ def update_epic_related_userstories_order_in_bulk(bulk_data: list, epic: object) [{'us_id': , 'order': }, ...] """ related_user_stories = epic.relateduserstory_set.all() + # select_related rus_orders = {rus.id: rus.order for rus in related_user_stories} rus_conversion = {rus.user_story_id: rus.id for rus in related_user_stories} new_rus_orders = {rus_conversion[e["us_id"]]: e["order"] for e in bulk_data - if e["us_id"] in rus_conversion} + if e["us_id"] in rus_conversion} apply_order_updates(rus_orders, new_rus_orders) @@ -274,7 +275,8 @@ def _get_epics_assigned_to(project, queryset): FROM projects_membership LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id") INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") - WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL + WHERE "projects_membership"."project_id" = %s + AND "projects_membership"."user_id" IS NOT NULL -- unassigned epics UNION @@ -336,7 +338,8 @@ def _get_epics_owners(project, queryset): FROM projects_membership LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") - WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL + WHERE "projects_membership"."project_id" = %s + AND "projects_membership"."user_id" IS NOT NULL -- System users UNION diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index 9183df1c..0becda53 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -675,34 +675,6 @@ def test_epic_action_bulk_create(client, data): assert results == [401, 403, 403, 451, 451] -def test_bulk_create_related_userstories(client, data): - public_url = reverse('epics-related-userstories-bulk-create', args=[data.public_epic.pk]) - private_url1 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic1.pk]) - private_url2 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic2.pk]) - blocked_url = reverse('epics-related-userstories-bulk-create', args=[data.blocked_epic.pk]) - - users = [ - None, - data.registered_user, - data.project_member_without_perms, - data.project_member_with_perms, - data.project_owner - ] - - bulk_data = json.dumps({ - "userstories": "test1\ntest2", - }) - - results = helper_test_http_method(client, 'post', public_url, bulk_data, users) - assert results == [401, 403, 403, 200, 200] - results = helper_test_http_method(client, 'post', private_url1, bulk_data, users) - assert results == [401, 403, 403, 200, 200] - results = helper_test_http_method(client, 'post', private_url2, bulk_data, users) - assert results == [401, 403, 403, 200, 200] - results = helper_test_http_method(client, 'post', blocked_url, bulk_data, users) - assert results == [401, 403, 403, 451, 451] - - def test_epic_action_upvote(client, data): public_url = reverse('epics-upvote', kwargs={"pk": data.public_epic.pk}) private_url1 = reverse('epics-upvote', kwargs={"pk": data.private_epic1.pk}) From 9e609ee20a6311dc5395e75c57d9fb38394e5bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 25 Aug 2016 12:11:16 +0200 Subject: [PATCH 219/261] Disabled epics module by default in Project and ProjectTemplates --- taiga/projects/migrations/0049_auto_20160629_1443.py | 4 ++-- taiga/projects/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/taiga/projects/migrations/0049_auto_20160629_1443.py b/taiga/projects/migrations/0049_auto_20160629_1443.py index 417875e5..8c2117b0 100644 --- a/taiga/projects/migrations/0049_auto_20160629_1443.py +++ b/taiga/projects/migrations/0049_auto_20160629_1443.py @@ -66,7 +66,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='project', name='is_epics_activated', - field=models.BooleanField(default=True, verbose_name='active epics panel'), + field=models.BooleanField(default=False, verbose_name='active epics panel'), ), migrations.AddField( model_name='projecttemplate', @@ -76,7 +76,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='projecttemplate', name='is_epics_activated', - field=models.BooleanField(default=True, verbose_name='active epics panel'), + field=models.BooleanField(default=False, verbose_name='active epics panel'), ), migrations.AlterField( model_name='project', diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 1642426c..fe62c015 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -165,7 +165,7 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): verbose_name=_("total of milestones")) total_story_points = models.FloatField(null=True, blank=True, verbose_name=_("total story points")) - is_epics_activated = models.BooleanField(default=True, null=False, blank=True, + is_epics_activated = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("active epics panel")) is_backlog_activated = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("active backlog panel")) @@ -741,7 +741,7 @@ class ProjectTemplate(models.Model): blank=False, verbose_name=_("default owner's role")) - is_epics_activated = models.BooleanField(default=True, null=False, blank=True, + is_epics_activated = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("active epics panel")) is_backlog_activated = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("active backlog panel")) From 75ddf706ee527862eac71ceb7dc5b804b47ad267 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 25 Aug 2016 14:17:28 +0200 Subject: [PATCH 220/261] Adding epic related userstories tests file --- ...est_epics_related_userstories_resources.py | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 tests/integration/resources_permissions/test_epics_related_userstories_resources.py diff --git a/tests/integration/resources_permissions/test_epics_related_userstories_resources.py b/tests/integration/resources_permissions/test_epics_related_userstories_resources.py new file mode 100644 index 00000000..d1924ec8 --- /dev/null +++ b/tests/integration/resources_permissions/test_epics_related_userstories_resources.py @@ -0,0 +1,392 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import uuid + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.epics.serializers import EpicRelatedUserStorySerializer +from taiga.projects.epics.models import Epic +from taiga.projects.epics.utils import attach_extra_info as attach_epic_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS +from taiga.projects.occ import OCCResourceMixin + +from tests import factories as f +from tests.utils import helper_test_http_method, reconnect_signals +from taiga.projects.votes.services import add_vote +from taiga.projects.notifications.services import add_watcher + +from unittest import mock + +import pytest +pytestmark = pytest.mark.django_db + + +def setup_function(function): + reconnect_signals() + + +@pytest.fixture +def data(): + m = type("Models", (object,), {}) + + m.registered_user = f.UserFactory.create() + m.project_member_with_perms = f.UserFactory.create() + m.project_member_without_perms = f.UserFactory.create() + m.project_owner = f.UserFactory.create() + m.other_user = f.UserFactory.create() + + m.public_project = f.ProjectFactory(is_private=False, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + + m.private_project1 = f.ProjectFactory(is_private=True, + anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + + m.private_project2 = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=m.blocked_project, + user=m.project_member_without_perms, + role__project=m.blocked_project, + role__permissions=[]) + + f.MembershipFactory(project=m.public_project, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project1, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.private_project2, + user=m.project_owner, + is_admin=True) + + f.MembershipFactory(project=m.blocked_project, + user=m.project_owner, + is_admin=True) + + m.public_epic = f.EpicFactory(project=m.public_project, + status__project=m.public_project) + m.public_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.public_epic.id) + + m.private_epic1 = f.EpicFactory(project=m.private_project1, + status__project=m.private_project1) + m.private_epic1 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic1.id) + + m.private_epic2 = f.EpicFactory(project=m.private_project2, + status__project=m.private_project2) + m.private_epic2 = attach_epic_extra_info(Epic.objects.all()).get(id=m.private_epic2.id) + + m.blocked_epic = f.EpicFactory(project=m.blocked_project, + status__project=m.blocked_project) + m.blocked_epic = attach_epic_extra_info(Epic.objects.all()).get(id=m.blocked_epic.id) + + + m.public_us = f.UserStoryFactory(project=m.public_project) + m.private_us1 = f.UserStoryFactory(project=m.private_project1) + m.private_us2 = f.UserStoryFactory(project=m.private_project2) + m.blocked_us = f.UserStoryFactory(project=m.blocked_project) + + m.public_related_us = f.RelatedUserStory(epic=m.public_epic, user_story=m.public_us) + m.private_related_us1 = f.RelatedUserStory(epic=m.private_epic1, user_story=m.private_us1) + m.private_related_us2 = f.RelatedUserStory(epic=m.private_epic2, user_story=m.private_us2) + m.blocked_related_us = f.RelatedUserStory(epic=m.blocked_epic, user_story=m.blocked_us) + + m.public_project.default_epic_status = m.public_epic.status + m.public_project.save() + m.private_project1.default_epic_status = m.private_epic1.status + m.private_project1.save() + m.private_project2.default_epic_status = m.private_epic2.status + m.private_project2.save() + m.blocked_project.default_epic_status = m.blocked_epic.status + m.blocked_project.save() + + return m + + +def test_epic_related_userstories_list(client, data): + url = reverse('epics-related-userstories-list', args=[data.public_epic.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + client.login(data.registered_user) + + url = reverse('epics-related-userstories-list', args=[data.private_epic1.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + client.login(data.project_member_with_perms) + + url = reverse('epics-related-userstories-list', args=[data.private_epic2.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + client.login(data.project_owner) + + url = reverse('epics-related-userstories-list', args=[data.blocked_epic.pk]) + response = client.get(url) + related_uss_data = json.loads(response.content.decode('utf-8')) + assert len(related_uss_data) == 1 + assert response.status_code == 200 + + +def test_epic_related_userstories_retrieve(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', public_url, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url1, None, users) + assert results == [200, 200, 200, 200, 200] + results = helper_test_http_method(client, 'get', private_url2, None, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'get', blocked_url, None, users) + assert results == [401, 403, 403, 200, 200] + + +def test_epic_related_userstories_create(client, data): + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + create_data = json.dumps({ + "user_story": data.public_us.id, + "epic": data.public_epic.id + }) + url = reverse('epics-related-userstories-list', args=[data.public_epic.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "user_story": data.private_us1.id, + "epic": data.private_epic1.id + }) + url = reverse('epics-related-userstories-list', args=[data.private_epic1.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "user_story": data.private_us2.id, + "epic": data.private_epic2.id + }) + url = reverse('epics-related-userstories-list', args=[data.private_epic2.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 201, 201] + + create_data = json.dumps({ + "user_story": data.blocked_us.id, + "epic": data.blocked_epic.id + }) + url = reverse('epics-related-userstories-list', args=[data.blocked_epic.pk]) + results = helper_test_http_method(client, 'post', url, create_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_related_userstories_put_update(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.public_related_us).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', public_url, epic_related_us_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.private_related_us1).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', private_url1, epic_related_us_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.private_related_us2).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', private_url2, epic_related_us_data, users) + assert results == [401, 403, 403, 200, 200] + + epic_related_us_data = EpicRelatedUserStorySerializer(data.blocked_related_us).data + epic_related_us_data["order"] = 33 + epic_related_us_data = json.dumps(epic_related_us_data) + results = helper_test_http_method(client, 'put', blocked_url, epic_related_us_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_related_userstories_patch_update(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + patch_data = json.dumps({"order": 33}) + + results = helper_test_http_method(client, 'patch', public_url, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url1, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', private_url2, patch_data, users) + assert results == [401, 403, 403, 200, 200] + + results = helper_test_http_method(client, 'patch', blocked_url, patch_data, users) + assert results == [401, 403, 403, 451, 451] + + +def test_epic_related_userstories_delete(client, data): + public_url = reverse('epics-related-userstories-detail', args=[data.public_epic.pk, data.public_us.pk]) + private_url1 = reverse('epics-related-userstories-detail', args=[data.private_epic1.pk, data.private_us1.pk]) + private_url2 = reverse('epics-related-userstories-detail', args=[data.private_epic2.pk, data.private_us2.pk]) + blocked_url = reverse('epics-related-userstories-detail', args=[data.blocked_epic.pk, data.blocked_us.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + ] + results = helper_test_http_method(client, 'delete', public_url, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url1, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', private_url2, None, users) + assert results == [401, 403, 403, 204] + results = helper_test_http_method(client, 'delete', blocked_url, None, users) + assert results == [401, 403, 403, 451] + + +def test_bulk_create_related_userstories(client, data): + public_url = reverse('epics-related-userstories-bulk-create', args=[data.public_epic.pk]) + private_url1 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic1.pk]) + private_url2 = reverse('epics-related-userstories-bulk-create', args=[data.private_epic2.pk]) + blocked_url = reverse('epics-related-userstories-bulk-create', args=[data.blocked_epic.pk]) + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + bulk_data = json.dumps({ + "userstories": "test1\ntest2", + }) + + results = helper_test_http_method(client, 'post', public_url, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url1, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', private_url2, bulk_data, users) + assert results == [401, 403, 403, 200, 200] + results = helper_test_http_method(client, 'post', blocked_url, bulk_data, users) + assert results == [401, 403, 403, 451, 451] From df174230a4792834da39a3ba33d7300620236dae Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 25 Aug 2016 14:18:22 +0200 Subject: [PATCH 221/261] Migration for adding epic status to existing projects --- taiga/projects/migrations/0052_epic_status.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 taiga/projects/migrations/0052_epic_status.py diff --git a/taiga/projects/migrations/0052_epic_status.py b/taiga/projects/migrations/0052_epic_status.py new file mode 100644 index 00000000..baa9ab46 --- /dev/null +++ b/taiga/projects/migrations/0052_epic_status.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-08-25 10:19 +from __future__ import unicode_literals + +from django.db import connection, migrations, models + +def update_epic_status(apps, schema_editor): + Project = apps.get_model("projects", "Project") + project_ids = Project.objects.filter(default_epic_status__isnull=True).values_list("id", flat=True) + if not project_ids: + return + + values_sql = [] + for project_id in project_ids: + values_sql.append("('New', 'new', 1, false, '#999999', {project_id})".format(project_id=project_id)) + values_sql.append("('Ready', 'ready', 2, false, '#ff8a84', {project_id})".format(project_id=project_id)) + values_sql.append("('In progress', 'in-progress', 3, false, '#ff9900', {project_id})".format(project_id=project_id)) + values_sql.append("('Ready for test', 'ready-for-test', 4, false, '#fcc000', {project_id})".format(project_id=project_id)) + values_sql.append("('Done', 'done', 5, true, '#669900', {project_id})".format(project_id=project_id)) + + sql = """ + INSERT INTO projects_epicstatus (name, slug, "order", is_closed, color, project_id) + VALUES + {values}; + """.format(values=','.join(values_sql)) + cursor = connection.cursor() + cursor.execute(sql) + + +def update_default_epic_status(apps, schema_editor): + sql = """ + UPDATE projects_project + SET default_epic_status_id = projects_epicstatus.id + FROM projects_epicstatus + WHERE + projects_project.default_epic_status_id IS NULL + AND + projects_epicstatus.order = 1 + AND + projects_epicstatus.project_id = projects_project.id; + """ + cursor = connection.cursor() + cursor.execute(sql) + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0051_auto_20160729_0802'), + ] + + operations = [ + migrations.RunPython(update_epic_status), + migrations.RunPython(update_default_epic_status) + ] From bf9aafaca3db2f0a0e527896ce73c00505add231 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 31 Aug 2016 07:53:41 +0200 Subject: [PATCH 222/261] Fixing HistoryEntry for epic edition --- taiga/projects/history/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index aafaa915..4697875c 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -264,8 +264,8 @@ class HistoryEntry(models.Model): "deleted": [], } - olduss = {x["id"]:x for x in self.diff["user_stories"][0]} - newuss = {x["id"]:x for x in self.diff["user_stories"][1]} + olduss = {x["id"]: x for x in self.diff["user_stories"][0]} + newuss = {x["id"]: x for x in self.diff["user_stories"][1]} for usid in set(tuple(olduss.keys()) + tuple(newuss.keys())): if usid in olduss and usid not in newuss: @@ -273,7 +273,7 @@ class HistoryEntry(models.Model): elif usid not in olduss and usid in newuss: user_stories["new"].append(newuss[usid]) - if user_stories["new"] or user_stories["changed"] or user_stories["deleted"]: + if user_stories["new"] or user_stories["deleted"]: value = user_stories elif key in self.values: From 3648043f3700a922b192d07bf3ce1864eb7cbb2d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 1 Sep 2016 12:23:00 +0200 Subject: [PATCH 223/261] Adding unique restriction for epics --- .../migrations/0003_auto_20160901_1021.py | 19 +++++++++++++++++++ taiga/projects/epics/models.py | 1 + 2 files changed, 20 insertions(+) create mode 100644 taiga/projects/epics/migrations/0003_auto_20160901_1021.py diff --git a/taiga/projects/epics/migrations/0003_auto_20160901_1021.py b/taiga/projects/epics/migrations/0003_auto_20160901_1021.py new file mode 100644 index 00000000..e23169f2 --- /dev/null +++ b/taiga/projects/epics/migrations/0003_auto_20160901_1021.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-01 10:21 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('epics', '0002_epic_color'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='relateduserstory', + unique_together=set([('user_story', 'epic')]), + ), + ] diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index 3d1a2ce4..64bdbb84 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -103,6 +103,7 @@ class RelatedUserStory(models.Model): verbose_name = "related user story" verbose_name_plural = "related user stories" ordering = ["user_story", "order", "id"] + unique_together = (("user_story", "epic"), ) def __str__(self): return "{0} - {1}".format(self.epic_id, self.user_story_id) From 68f6cf08b290d8fff6c99f5007c7b9cfb088020d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 2 Sep 2016 10:47:06 +0200 Subject: [PATCH 224/261] Add epics to the sitemap generator --- taiga/front/sitemaps/__init__.py | 6 ++++ taiga/front/sitemaps/epics.py | 54 ++++++++++++++++++++++++++++++++ taiga/front/sitemaps/projects.py | 28 +++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 taiga/front/sitemaps/epics.py diff --git a/taiga/front/sitemaps/__init__.py b/taiga/front/sitemaps/__init__.py index abc78ffe..8c7adfa8 100644 --- a/taiga/front/sitemaps/__init__.py +++ b/taiga/front/sitemaps/__init__.py @@ -21,11 +21,14 @@ from collections import OrderedDict from .generics import GenericSitemap from .projects import ProjectsSitemap +from .projects import ProjectEpicsSitemap from .projects import ProjectBacklogsSitemap from .projects import ProjectKanbansSitemap from .projects import ProjectIssuesSitemap from .projects import ProjectTeamsSitemap +from .epics import EpicsSitemap + from .milestones import MilestonesSitemap from .userstories import UserStoriesSitemap @@ -43,11 +46,14 @@ sitemaps = OrderedDict([ ("generics", GenericSitemap), ("projects", ProjectsSitemap), + ("project-epics-list", ProjectEpicsSitemap), ("project-backlogs", ProjectBacklogsSitemap), ("project-kanbans", ProjectKanbansSitemap), ("project-issues-list", ProjectIssuesSitemap), ("project-teams", ProjectTeamsSitemap), + ("epics", EpicsSitemap), + ("milestones", MilestonesSitemap), ("userstories", UserStoriesSitemap), diff --git a/taiga/front/sitemaps/epics.py b/taiga/front/sitemaps/epics.py new file mode 100644 index 00000000..81f391f6 --- /dev/null +++ b/taiga/front/sitemaps/epics.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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.db.models import Q +from django.apps import apps + +from taiga.front.templatetags.functions import resolve + +from .base import Sitemap + + +class EpicsSitemap(Sitemap): + def items(self): + epic_model = apps.get_model("epics", "Epic") + + # Get epics of public projects OR private projects if anon user can view them + queryset = epic_model.objects.filter(Q(project__is_private=False) | + Q(project__is_private=True, + project__anon_permissions__contains=["view_epics"])) + + # Exclude blocked projects + queryset = queryset.filter(project__blocked_code__isnull=True) + + # Project data is needed + queryset = queryset.select_related("project") + + return queryset + + def location(self, obj): + return resolve("epic", obj.project.slug, obj.ref) + + def lastmod(self, obj): + return obj.modified_date + + def changefreq(self, obj): + return "daily" + + def priority(self, obj): + return 0.4 diff --git a/taiga/front/sitemaps/projects.py b/taiga/front/sitemaps/projects.py index a7e45d50..77785928 100644 --- a/taiga/front/sitemaps/projects.py +++ b/taiga/front/sitemaps/projects.py @@ -51,6 +51,34 @@ class ProjectsSitemap(Sitemap): return 0.9 +class ProjectEpicsSitemap(Sitemap): + def items(self): + project_model = apps.get_model("projects", "Project") + + # Get public projects OR private projects if anon user can view them and epics + queryset = project_model.objects.filter(Q(is_private=False) | + Q(is_private=True, + anon_permissions__contains=["view_project", + "view_epics"])) + + # Exclude projects without epics enabled + queryset = queryset.exclude(is_epics_activated=False) + + return queryset + + def location(self, obj): + return resolve("epics", obj.slug) + + def lastmod(self, obj): + return obj.modified_date + + def changefreq(self, obj): + return "daily" + + def priority(self, obj): + return 0.6 + + class ProjectBacklogsSitemap(Sitemap): def items(self): project_model = apps.get_model("projects", "Project") From b975a228592a1ab62bedb3b87c8dff86e6d989c5 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 5 Sep 2016 09:56:12 +0200 Subject: [PATCH 225/261] Allow setting project when creating related user stories for epics in bulk mode --- taiga/projects/epics/api.py | 7 +++---- taiga/projects/epics/validators.py | 8 +++++--- tests/integration/test_epics.py | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 5888865d..599252ce 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -263,7 +263,6 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod return services.update_epic_related_userstories_order_in_bulk(data, epic=obj.epic) - def post_save(self, obj, created=False): if not created: # Let's reorder the related stuff after edit the element @@ -276,21 +275,21 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): - validator = validators.CrateRelatedUserStoriesBulkValidator(data=request.DATA) + validator = validators.CreateRelatedUserStoriesBulkValidator(data=request.DATA) if not validator.is_valid(): return response.BadRequest(validator.errors) data = validator.data epic = get_object_or_404(models.Epic, id=kwargs["epic"]) - project = epic.project + project = Project.objects.get(pk=data.get('project_id')) self.check_permissions(request, 'bulk_create', project) if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) services.create_related_userstories_in_bulk( - data["userstories"], + data["bulk_userstories"], epic, project=project, owner=request.user diff --git a/taiga/projects/epics/validators.py b/taiga/projects/epics/validators.py index 175f1c3c..7ed00481 100644 --- a/taiga/projects/epics/validators.py +++ b/taiga/projects/epics/validators.py @@ -55,9 +55,11 @@ class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator, bulk_epics = serializers.CharField() -class CrateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator, - validators.Validator): - userstories = serializers.CharField() +class CreateRelatedUserStoriesBulkValidator(ProjectExistsValidator, EpicExistsValidator, + validators.Validator): + project_id = serializers.IntegerField() + bulk_userstories = serializers.CharField() + class EpicRelatedUserStoryValidator(validators.ModelValidator): diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 0ca87d09..27314f02 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -101,7 +101,8 @@ def test_bulk_create_related_userstories(client): url = reverse('epics-related-userstories-bulk-create', args=[epic.pk]) data = { - "userstories": "test1\ntest2" + "bulk_userstories": "test1\ntest2", + "project_id": project.id } client.login(user) response = client.json.post(url, json.dumps(data)) From f2a800a099ae9ecd0b36c3efbb7948a4c27a9dca Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 8 Sep 2016 15:12:25 +0200 Subject: [PATCH 226/261] Fixing issue on epic related userstories update --- taiga/projects/epics/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 599252ce..81dd242e 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -257,7 +257,7 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod return {} extra_orders = json.loads(self.request.META.get("HTTP_SET_ORDERS", "{}")) - data = [{"us_id": obj.id, "order": getattr(obj, "order")}] + data = [{"us_id": obj.user_story.id, "order": getattr(obj, "order")}] for id, order in extra_orders.items(): data.append({"us_id": int(id), "order": order}) From 35b799e89d52a9c5a103bab396232791aad79171 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 9 Sep 2016 14:35:12 +0200 Subject: [PATCH 227/261] Adding color to epic freezer --- taiga/projects/history/freeze_impl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 1c2fae44..489f2c79 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -295,6 +295,7 @@ def milestone_freezer(milestone) -> dict: def epic_freezer(epic) -> dict: snapshot = { "ref": epic.ref, + "color": epic.color, "owner": epic.owner_id, "status": epic.status.id if epic.status else None, "epics_order": epic.epics_order, From db34ad32c98faf1d15e6c88eac37c47cf55e749d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 12 Sep 2016 14:10:50 +0200 Subject: [PATCH 228/261] Including epic in timelines --- taiga/projects/epics/api.py | 11 +++++++---- taiga/projects/epics/models.py | 10 +++++++++- taiga/projects/epics/services.py | 2 +- taiga/projects/history/freeze_impl.py | 15 ++++++++++++++- taiga/projects/history/services.py | 4 ++++ taiga/projects/mixins/serializers.py | 3 ++- taiga/projects/notifications/services.py | 1 + taiga/timeline/service.py | 17 +++++++++++++++-- taiga/timeline/timeline_implementations.py | 16 +++++++++++++++- 9 files changed, 68 insertions(+), 11 deletions(-) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index 81dd242e..fce31802 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -22,7 +22,7 @@ from django.utils.translation import ugettext as _ from taiga.base.api.utils import get_object_or_404 from taiga.base import filters, response from taiga.base import exceptions as exc -from taiga.base.decorators import list_route, detail_route +from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.viewsets import NestedViewSetMixin @@ -33,7 +33,6 @@ from taiga.projects.models import Project, EpicStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin -from taiga.projects.userstories.models import UserStory from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from . import models @@ -226,7 +225,8 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, return response.Ok(epics_serialized.data) -class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, ModelCrudViewSet): +class EpicRelatedUserStoryViewSet(NestedViewSetMixin, HistoryResourceMixin, + BlockedByProjectMixin, ModelCrudViewSet): queryset = models.RelatedUserStory.objects.all() serializer_class = serializers.EpicRelatedUserStorySerializer validator_class = validators.EpicRelatedUserStoryValidator @@ -288,13 +288,16 @@ class EpicRelatedUserStoryViewSet(NestedViewSetMixin, BlockedByProjectMixin, Mod if project.blocked_code is not None: raise exc.Blocked(_("Blocked element")) - services.create_related_userstories_in_bulk( + related_userstories = services.create_related_userstories_in_bulk( data["bulk_userstories"], epic, project=project, owner=request.user ) + for related_userstory in related_userstories: + self.persist_history_snapshot(obj=related_userstory) + related_uss_serialized = self.get_serializer_class()(epic.relateduserstory_set.all(), many=True) return response.Ok(related_uss_serialized.data) diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index 64bdbb84..f41cec30 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -92,7 +92,7 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M super().save(*args, **kwargs) -class RelatedUserStory(models.Model): +class RelatedUserStory(WatchedModelMixin, models.Model): user_story = models.ForeignKey("userstories.UserStory", on_delete=models.CASCADE) epic = models.ForeignKey("epics.Epic", on_delete=models.CASCADE) @@ -111,3 +111,11 @@ class RelatedUserStory(models.Model): @property def project(self): return self.epic.project + + @property + def owner_id(self): + return self.epic.owner_id + + @property + def assigned_to_id(self): + return self.epic.assigned_to_id diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index bf0edff5..610bfcfb 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -124,7 +124,7 @@ def create_related_userstories_in_bulk(bulk_data, epic, **additional_fields): finally: connect_userstories_signals() - return userstories + return related_userstories def update_epic_related_userstories_order_in_bulk(bulk_data: list, epic: object): diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 489f2c79..6804c249 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -112,8 +112,11 @@ def epic_values(diff): if "status" in diff: values["status"] = _get_us_status_values(diff["status"]) - # TODO EPICS: What happen with usr stories? + return values + +def epic_related_userstory_values(diff): + values = _common_users_values(diff) return values @@ -317,6 +320,16 @@ def epic_freezer(epic) -> dict: return snapshot +def epic_related_userstory_freezer(related_us) -> dict: + snapshot = { + "user_story": related_us.user_story.id, + "epic": related_us.epic.id, + "order": related_us.order + } + + return snapshot + + def userstory_freezer(us) -> dict: rp_cls = apps.get_model("userstories", "RolePoints") rpqsd = rp_cls.objects.filter(user_story=us) diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 54dd78fc..019cf258 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -51,6 +51,7 @@ from .models import HistoryType from .freeze_impl import project_freezer from .freeze_impl import milestone_freezer from .freeze_impl import epic_freezer +from .freeze_impl import epic_related_userstory_freezer from .freeze_impl import userstory_freezer from .freeze_impl import issue_freezer from .freeze_impl import task_freezer @@ -60,6 +61,7 @@ from .freeze_impl import wikipage_freezer from .freeze_impl import project_values from .freeze_impl import milestone_values from .freeze_impl import epic_values +from .freeze_impl import epic_related_userstory_values from .freeze_impl import userstory_values from .freeze_impl import issue_values from .freeze_impl import task_values @@ -397,6 +399,7 @@ def prefetch_owners_in_history_queryset(qs): register_freeze_implementation("projects.project", project_freezer) register_freeze_implementation("milestones.milestone", milestone_freezer,) register_freeze_implementation("epics.epic", epic_freezer) +register_freeze_implementation("epics.relateduserstory", epic_related_userstory_freezer) register_freeze_implementation("userstories.userstory", userstory_freezer) register_freeze_implementation("issues.issue", issue_freezer) register_freeze_implementation("tasks.task", task_freezer) @@ -405,6 +408,7 @@ register_freeze_implementation("wiki.wikipage", wikipage_freezer) register_values_implementation("projects.project", project_values) register_values_implementation("milestones.milestone", milestone_values) register_values_implementation("epics.epic", epic_values) +register_values_implementation("epics.relateduserstory", epic_related_userstory_values) register_values_implementation("userstories.userstory", userstory_values) register_values_implementation("issues.issue", issue_values) register_values_implementation("tasks.task", task_values) diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index 67949877..c8a70932 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -98,7 +98,8 @@ class ProjectExtraInfoSerializerMixin(serializers.LightSerializer): serialized_project = { "name": obj.project.name, "slug": obj.project.slug, - "logo_small_url": services.get_logo_small_thumbnail_url(obj.project) + "logo_small_url": services.get_logo_small_thumbnail_url(obj.project), + "id": obj.project_id } self._serialized_project[obj.project_id] = serialized_project diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 4c7012d1..e998d068 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -336,6 +336,7 @@ def get_related_people(obj): related_people = related_people.exclude(is_active=False) related_people = related_people.exclude(is_system=True) related_people = related_people.distinct() + return related_people diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 94d37c80..03ca3e96 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -94,6 +94,7 @@ def _push_to_timeline(objects, instance: object, event_type: str, created_dateti @app.task def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type, created_datetime, extra_data={}): + ObjModel = apps.get_model(obj_app_label, obj_model_name) try: obj = ObjModel.objects.get(id=obj_id) @@ -266,13 +267,25 @@ def extract_epic_info(instance): } -def extract_userstory_info(instance): - return { +def extract_userstory_info(instance, include_project=False): + userstory_info = { "id": instance.pk, "ref": instance.ref, "subject": instance.subject, } + if include_project: + userstory_info["project"] = extract_project_info(instance.project) + + return userstory_info + + +def extract_related_userstory_info(instance): + return { + "id": instance.pk, + "subject": instance.user_story.subject + } + def extract_issue_info(instance): return { diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py index 9b97fa06..1d480e82 100644 --- a/taiga/timeline/timeline_implementations.py +++ b/taiga/timeline/timeline_implementations.py @@ -47,7 +47,7 @@ def milestone_timeline(instance, extra_data={}): @register_timeline_implementation("epics.epic", "change") @register_timeline_implementation("epics.epic", "delete") def epic_timeline(instance, extra_data={}): - result ={ + result = { "epic": service.extract_epic_info(instance), "project": service.extract_project_info(instance.project), } @@ -55,6 +55,20 @@ def epic_timeline(instance, extra_data={}): return result +@register_timeline_implementation("epics.relateduserstory", "create") +@register_timeline_implementation("epics.relateduserstory", "change") +@register_timeline_implementation("epics.relateduserstory", "delete") +def epic_related_userstory_timeline(instance, extra_data={}): + result = { + "relateduserstory": service.extract_related_userstory_info(instance), + "epic": service.extract_epic_info(instance.epic), + "userstory": service.extract_userstory_info(instance.user_story, include_project=True), + "project": service.extract_project_info(instance.project), + } + result.update(extra_data) + return result + + @register_timeline_implementation("userstories.userstory", "create") @register_timeline_implementation("userstories.userstory", "change") @register_timeline_implementation("userstories.userstory", "delete") From e9ca1abf557060e73010d513624b0410e47da10d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 12 Sep 2016 15:34:15 +0200 Subject: [PATCH 229/261] Adding epics to user favorites and voted APIs --- taiga/users/services.py | 10 ++++++- tests/integration/test_users.py | 46 ++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/taiga/users/services.py b/taiga/users/services.py index 5923ff11..4b49353e 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -325,6 +325,8 @@ def get_watched_list(for_user, from_user, type=None, q=None): row_to_json(users_user) as assigned_to_extra_info FROM ( + {epics_sql} + UNION {userstories_sql} UNION {tasks_sql} @@ -365,6 +367,7 @@ def get_watched_list(for_user, from_user, type=None, q=None): OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) OR (entities.type = 'project' AND 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'epic' AND 'view_epic' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) ) )) -- END Permissions checking @@ -384,6 +387,7 @@ def get_watched_list(for_user, from_user, type=None, q=None): userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "notifications_watched", slug_column="null"), tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "notifications_watched", slug_column="null"), issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "notifications_watched", slug_column="null"), + epics_sql=_build_sql_for_type(for_user, "epic", "epics_epic", "notifications_watched", slug_column="null"), projects_sql=_build_watched_sql_for_projects(for_user)) cursor = connection.cursor() @@ -503,6 +507,8 @@ def get_voted_list(for_user, from_user, type=None, q=None): users_user.id as assigned_to_id, row_to_json(users_user) as assigned_to_extra_info FROM ( + {epics_sql} + UNION {userstories_sql} UNION {tasks_sql} @@ -540,6 +546,7 @@ def get_voted_list(for_user, from_user, type=None, q=None): (entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) + OR (entities.type = 'epic' AND 'view_epic' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions))) ) )) -- END Permissions checking @@ -558,7 +565,8 @@ def get_voted_list(for_user, from_user, type=None, q=None): filters_sql=filters_sql, userstories_sql=_build_sql_for_type(for_user, "userstory", "userstories_userstory", "votes_vote", slug_column="null"), tasks_sql=_build_sql_for_type(for_user, "task", "tasks_task", "votes_vote", slug_column="null"), - issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "votes_vote", slug_column="null")) + issues_sql=_build_sql_for_type(for_user, "issue", "issues_issue", "votes_vote", slug_column="null"), + epics_sql=_build_sql_for_type(for_user, "epic", "epics_epic", "votes_vote", slug_column="null")) cursor = connection.cursor() params = { diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index c9bf5fba..3accebb7 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -454,6 +454,9 @@ def test_get_watched_list(): membership = f.MembershipFactory(project=project, role=role, user=fav_user) project.add_watcher(fav_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + epic.add_watcher(fav_user) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") user_story.add_watcher(fav_user) @@ -463,11 +466,12 @@ def test_get_watched_list(): issue = f.IssueFactory(project=project, subject="Testing issue") issue.add_watcher(fav_user) - assert len(get_watched_list(fav_user, viewer_user)) == 4 + assert len(get_watched_list(fav_user, viewer_user)) == 5 assert len(get_watched_list(fav_user, viewer_user, type="project")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="userstory")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="task")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="issue")) == 1 + assert len(get_watched_list(fav_user, viewer_user, type="epic")) == 1 assert len(get_watched_list(fav_user, viewer_user, type="unknown")) == 0 assert len(get_watched_list(fav_user, viewer_user, q="issue")) == 1 @@ -500,6 +504,11 @@ def test_get_voted_list(): role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=fav_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + content_type = ContentType.objects.get_for_model(epic) + f.VoteFactory(content_type=content_type, object_id=epic.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=epic.id, count=1) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") content_type = ContentType.objects.get_for_model(user_story) f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user) @@ -515,7 +524,8 @@ def test_get_voted_list(): f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user) f.VotesFactory(content_type=content_type, object_id=issue.id, count=1) - assert len(get_voted_list(fav_user, viewer_user)) == 3 + assert len(get_voted_list(fav_user, viewer_user)) == 4 + assert len(get_voted_list(fav_user, viewer_user, type="epic")) == 1 assert len(get_voted_list(fav_user, viewer_user, type="userstory")) == 1 assert len(get_voted_list(fav_user, viewer_user, type="task")) == 1 assert len(get_voted_list(fav_user, viewer_user, type="issue")) == 1 @@ -530,7 +540,7 @@ def test_get_watched_list_valid_info_for_project(): viewer_user = f.UserFactory() project = f.ProjectFactory(is_private=False, name="Testing project") - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) project.add_watcher(fav_user) raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0] @@ -568,7 +578,7 @@ def test_get_watched_list_for_project_with_ignored_notify_level(): viewer_user = f.UserFactory() project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=fav_user) notify_policy = NotifyPolicy.objects.get(user=fav_user, project=project) notify_policy.notify_level=NotifyLevel.none @@ -624,6 +634,7 @@ def test_get_watched_list_valid_info_for_not_project_types(): project = f.ProjectFactory(is_private=False, name="Testing project") factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -680,6 +691,7 @@ def test_get_voted_list_valid_info(): project = f.ProjectFactory(is_private=False, name="Testing project") factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -743,6 +755,7 @@ def test_get_watched_list_with_liked_and_voted_objects(client): f.LikeFactory(content_type=content_type, object_id=project.id, user=fav_user) voted_elements_factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -793,6 +806,7 @@ def test_get_voted_list_with_watched_objects(client): membership = f.MembershipFactory(project=project, role=role, user=fav_user) voted_elements_factories = { + "epic": f.EpicFactory, "userstory": f.UserStoryFactory, "task": f.TaskFactory, "issue": f.IssueFactory @@ -820,9 +834,12 @@ def test_get_watched_list_permissions(): project = f.ProjectFactory(is_private=True, name="Testing project") project.add_watcher(fav_user) - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + epic.add_watcher(fav_user) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") user_story.add_watcher(fav_user) @@ -838,13 +855,13 @@ def test_get_watched_list_permissions(): #If the project is private but the viewer user has permissions the votes should # be accesible - assert len(get_watched_list(fav_user, viewer_priviliged_user)) == 4 + assert len(get_watched_list(fav_user, viewer_priviliged_user)) == 5 #If the project is private but has the required anon permissions the votes should # be accesible by any user too - project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.anon_permissions = ["view_project", "view_epic", "view_us", "view_tasks", "view_issues"] project.save() - assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 4 + assert len(get_watched_list(fav_user, viewer_unpriviliged_user)) == 5 def test_get_liked_list_permissions(): @@ -879,9 +896,14 @@ def test_get_voted_list_permissions(): viewer_priviliged_user = f.UserFactory() project = f.ProjectFactory(is_private=True, name="Testing project") - role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"]) + role = f.RoleFactory(project=project, permissions=["view_project", "view_epic", "view_us", "view_tasks", "view_issues"]) membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user) + epic = f.EpicFactory(project=project, subject="Testing epic") + content_type = ContentType.objects.get_for_model(epic) + f.VoteFactory(content_type=content_type, object_id=epic.id, user=fav_user) + f.VotesFactory(content_type=content_type, object_id=epic.id, count=1) + user_story = f.UserStoryFactory(project=project, subject="Testing user story") content_type = ContentType.objects.get_for_model(user_story) f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user) @@ -903,10 +925,10 @@ def test_get_voted_list_permissions(): #If the project is private but the viewer user has permissions the votes should # be accesible - assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 3 + assert len(get_voted_list(fav_user, viewer_priviliged_user)) == 4 #If the project is private but has the required anon permissions the votes should # be accesible by any user too - project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"] + project.anon_permissions = ["view_project", "view_epic", "view_us", "view_tasks", "view_issues"] project.save() - assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 3 + assert len(get_voted_list(fav_user, viewer_unpriviliged_user)) == 4 From 8475015025eabf38bdffa1e1643a7ea094a5c567 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 13 Sep 2016 09:11:21 +0200 Subject: [PATCH 230/261] Adding timeline entries on bulk_create calls --- taiga/projects/epics/api.py | 3 +++ taiga/projects/tasks/api.py | 3 +++ taiga/projects/userstories/api.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index fce31802..fed57abd 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -220,6 +220,9 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, callback=self.post_save, precall=self.pre_save) epics = self.get_queryset().filter(id__in=[i.id for i in epics]) + for epic in epics: + self.persist_history_snapshot(obj=epic) + epics_serialized = self.get_serializer_class()(epics, many=True) return response.Ok(epics_serialized.data) diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index e722f935..778e080d 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -266,6 +266,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) tasks = self.get_queryset().filter(id__in=[i.id for i in tasks]) + for task in tasks: + self.persist_history_snapshot(obj=task) + tasks_serialized = self.get_serializer_class()(tasks, many=True) return response.Ok(tasks_serialized.data) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 758fa98c..df1b495a 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -340,6 +340,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi callback=self.post_save, precall=self.pre_save) user_stories = self.get_queryset().filter(id__in=[i.id for i in user_stories]) + for user_story in user_stories: + self.persist_history_snapshot(obj=user_story) + user_stories_serialized = self.get_serializer_class()(user_stories, many=True) return response.Ok(user_stories_serialized.data) From 21a7ca3e4c9a466b79e250d3dc5b5dc2029e14de Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 13 Sep 2016 10:39:17 +0200 Subject: [PATCH 231/261] Disabling epics module by default for new projects --- taiga/projects/fixtures/initial_project_templates.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/fixtures/initial_project_templates.json b/taiga/projects/fixtures/initial_project_templates.json index 9bc6d969..6137792f 100644 --- a/taiga/projects/fixtures/initial_project_templates.json +++ b/taiga/projects/fixtures/initial_project_templates.json @@ -10,7 +10,7 @@ "created_date": "2014-04-22T14:48:43.596Z", "modified_date": "2016-08-24T16:26:40.845Z", "default_owner_role": "product-owner", - "is_epics_activated": true, + "is_epics_activated": false, "is_backlog_activated": true, "is_kanban_activated": false, "is_wiki_activated": true, @@ -40,7 +40,7 @@ "created_date": "2014-04-22T14:50:19.738Z", "modified_date": "2016-08-24T16:26:45.365Z", "default_owner_role": "product-owner", - "is_epics_activated": true, + "is_epics_activated": false, "is_backlog_activated": false, "is_kanban_activated": true, "is_wiki_activated": false, From 0b6a31271e69d3006d1d8422800d9402b6d691c8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 13 Sep 2016 15:15:05 +0200 Subject: [PATCH 232/261] Fixing HistoryEntry generation for epic changes --- taiga/projects/epics/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index f41cec30..09003097 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -112,6 +112,10 @@ class RelatedUserStory(WatchedModelMixin, models.Model): def project(self): return self.epic.project + @property + def project_id(self): + return self.epic.project_id + @property def owner_id(self): return self.epic.owner_id From 564cd4350494fd377061dcbc73ce11d785803253 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 13 Sep 2016 15:19:30 +0200 Subject: [PATCH 233/261] Excluding epics_order from diffs --- taiga/projects/history/services.py | 1 + 1 file changed, 1 insertion(+) diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 019cf258..7247ccdd 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -80,6 +80,7 @@ _values_impl_map = {} # Not important fields for models (history entries with only # this fields are marked as hidden). _not_important_fields = { + "epics.epic": frozenset(["epics_order"]), "userstories.userstory": frozenset(["backlog_order", "sprint_order", "kanban_order"]), "tasks.task": frozenset(["us_order", "taskboard_order"]), } From 4e5013523482a4a27ebb557c0fe40c70b1b7c5b9 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 15 Sep 2016 12:20:48 +0200 Subject: [PATCH 234/261] Support for updating epic status via commit message --- taiga/hooks/event_hooks.py | 8 ++- taiga/projects/epics/models.py | 6 +- taiga/projects/history/services.py | 2 +- ...est_epics_related_userstories_resources.py | 33 +++++++--- tests/integration/test_hooks_bitbucket.py | 63 +++++++++++++++++++ tests/integration/test_hooks_github.py | 21 +++++++ tests/integration/test_hooks_gitlab.py | 25 ++++++++ tests/integration/test_hooks_gogs.py | 31 +++++++++ 8 files changed, 176 insertions(+), 13 deletions(-) diff --git a/taiga/hooks/event_hooks.py b/taiga/hooks/event_hooks.py index 85ac892f..93deb518 100644 --- a/taiga/hooks/event_hooks.py +++ b/taiga/hooks/event_hooks.py @@ -20,7 +20,8 @@ import re from django.utils.translation import ugettext as _ from django.contrib.auth import get_user_model -from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus +from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus, EpicStatus +from taiga.projects.epics.models import Epic from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -189,7 +190,10 @@ class BasePushEventHook(BaseEventHook): return _simple_status_change_message.format(platform=self.platform) def get_item_classes(self, ref): - if Issue.objects.filter(project=self.project, ref=ref).exists(): + if Epic.objects.filter(project=self.project, ref=ref).exists(): + modelClass = Epic + statusClass = EpicStatus + elif Issue.objects.filter(project=self.project, ref=ref).exists(): modelClass = Issue statusClass = IssueStatus elif Task.objects.filter(project=self.project, ref=ref).exists(): diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index 09003097..c2e26d20 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -115,7 +115,11 @@ class RelatedUserStory(WatchedModelMixin, models.Model): @property def project_id(self): return self.epic.project_id - + + @property + def owner(self): + return self.epic.owner + @property def owner_id(self): return self.epic.owner_id diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 7247ccdd..1bf27dee 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -80,7 +80,7 @@ _values_impl_map = {} # Not important fields for models (history entries with only # this fields are marked as hidden). _not_important_fields = { - "epics.epic": frozenset(["epics_order"]), + "epics.epic": frozenset(["epics_order", "user_stories"]), "userstories.userstory": frozenset(["backlog_order", "sprint_order", "kanban_order"]), "tasks.task": frozenset(["us_order", "taskboard_order"]), } diff --git a/tests/integration/resources_permissions/test_epics_related_userstories_resources.py b/tests/integration/resources_permissions/test_epics_related_userstories_resources.py index d1924ec8..04dd28c1 100644 --- a/tests/integration/resources_permissions/test_epics_related_userstories_resources.py +++ b/tests/integration/resources_permissions/test_epics_related_userstories_resources.py @@ -242,31 +242,31 @@ def test_epic_related_userstories_create(client, data): ] create_data = json.dumps({ - "user_story": data.public_us.id, + "user_story": f.UserStoryFactory(project=data.public_project).id, "epic": data.public_epic.id }) url = reverse('epics-related-userstories-list', args=[data.public_epic.pk]) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] + assert results == [401, 403, 403, 201, 400] create_data = json.dumps({ - "user_story": data.private_us1.id, + "user_story": f.UserStoryFactory(project=data.private_project1).id, "epic": data.private_epic1.id }) url = reverse('epics-related-userstories-list', args=[data.private_epic1.pk]) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] + assert results == [401, 403, 403, 201, 400] create_data = json.dumps({ - "user_story": data.private_us2.id, + "user_story": f.UserStoryFactory(project=data.private_project2).id, "epic": data.private_epic2.id }) url = reverse('epics-related-userstories-list', args=[data.private_epic2.pk]) results = helper_test_http_method(client, 'post', url, create_data, users) - assert results == [401, 403, 403, 201, 201] + assert results == [401, 403, 403, 201, 400] create_data = json.dumps({ - "user_story": data.blocked_us.id, + "user_story": f.UserStoryFactory(project=data.blocked_project).id, "epic": data.blocked_epic.id }) url = reverse('epics-related-userstories-list', args=[data.blocked_epic.pk]) @@ -379,14 +379,29 @@ def test_bulk_create_related_userstories(client, data): ] bulk_data = json.dumps({ - "userstories": "test1\ntest2", + "bulk_userstories": "test1\ntest2", + "project_id": data.public_project.id }) - results = helper_test_http_method(client, 'post', public_url, bulk_data, users) assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.private_project1.id + }) results = helper_test_http_method(client, 'post', private_url1, bulk_data, users) assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.private_project2.id + }) results = helper_test_http_method(client, 'post', private_url2, bulk_data, users) assert results == [401, 403, 403, 200, 200] + + bulk_data = json.dumps({ + "bulk_userstories": "test1\ntest2", + "project_id": data.blocked_project.id + }) results = helper_test_http_method(client, 'post', blocked_url, bulk_data, users) assert results == [401, 403, 403, 451, 451] diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 90023b38..6dbe06c3 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -31,6 +31,7 @@ from taiga.hooks.bitbucket import event_hooks from taiga.hooks.bitbucket.api import BitBucketViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.epics.models import Epic from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -239,6 +240,38 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s #%s ok bye!" % (epic.ref, new_status.slug) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -534,6 +567,36 @@ def test_push_event_task_bad_processing_non_existing_ref(client): assert len(mail.outbox) == 0 +def test_push_event_task_bad_processing_non_existing_ref(client): + issue_status = f.IssueStatusFactory() + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-6666666 #%s ok bye!" % (issue_status.slug) } + ] + } + ] + } + } + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue_status.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The referenced element doesn't exist" + assert len(mail.outbox) == 0 + + def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() payload = { diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 815aba49..1bcbcdbe 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -29,6 +29,7 @@ from taiga.hooks.github import event_hooks from taiga.hooks.github.api import GitHubViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.epics.models import Epic from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -111,6 +112,26 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s #%s ok + bye! + """ % (epic.ref, new_status.slug)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index 5208a939..b40c94e6 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -30,6 +30,7 @@ from taiga.hooks.gitlab import event_hooks from taiga.hooks.gitlab.api import GitLabViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.epics.models import Epic from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -446,6 +447,30 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s #%s ok + bye! + """ % (epic.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) diff --git a/tests/integration/test_hooks_gogs.py b/tests/integration/test_hooks_gogs.py index 290bc3f8..46c33dd9 100644 --- a/tests/integration/test_hooks_gogs.py +++ b/tests/integration/test_hooks_gogs.py @@ -29,6 +29,7 @@ from taiga.hooks.gogs import event_hooks from taiga.hooks.gogs.api import GogsViewSet from taiga.hooks.exceptions import ActionSyntaxException from taiga.projects import choices as project_choices +from taiga.projects.epics.models import Epic from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task from taiga.projects.userstories.models import UserStory @@ -120,6 +121,36 @@ def test_push_event_detected(client): assert response.status_code == 204 +def test_push_event_epic_processing(client): + creation_status = f.EpicStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_epics"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.EpicStatusFactory(project=creation_status.project) + epic = f.EpicFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (epic.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(epic.project, payload) + ev_hook.process_event() + epic = Epic.objects.get(id=epic.id) + assert epic.status.id == new_status.id + assert len(mail.outbox) == 1 + + def test_push_event_issue_processing(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) From 778bd6018991c896f76d19bdb65dc61ec44388dd Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 15 Sep 2016 13:29:10 +0200 Subject: [PATCH 235/261] Epic support in webhooks --- taiga/webhooks/serializers.py | 68 +++++ taiga/webhooks/tasks.py | 10 +- tests/integration/test_webhooks_epics.py | 319 +++++++++++++++++++++++ 3 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 tests/integration/test_webhooks_epics.py diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index 3525c973..7f6736f4 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -185,6 +185,26 @@ class RolePointsSerializer(serializers.LightSerializer): return obj.points.value +class EpicStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() + + def get_name(self, obj): + return obj.name + + def get_slug(self, obj): + return obj.slug + + def get_color(self, obj): + return obj.color + + def get_is_closed(self, obj): + return obj.is_closed + + class UserStoryStatusSerializer(serializers.LightSerializer): id = Field(attr="pk") name = MethodField() @@ -445,3 +465,51 @@ class WikiPageSerializer(serializers.LightSerializer): def get_permalink(self, obj): return resolve_front_url("wiki", obj.project.slug, obj.slug) + + +######################################################################## +# Epic +######################################################################## + +class EpicSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + created_date = Field() + modified_date = Field() + subject = Field() + watchers = MethodField() + description = Field() + tags = Field() + permalink = serializers.SerializerMethodField("get_permalink") + project = ProjectSerializer() + owner = UserSerializer() + assigned_to = UserSerializer() + status = EpicStatusSerializer() + epics_order = Field() + color = Field() + client_requirement = Field() + team_requirement = Field() + client_requirement = Field() + team_requirement = Field() + + def get_permalink(self, obj): + return resolve_front_url("epic", obj.project.slug, obj.ref) + + def custom_attributes_queryset(self, project): + return project.epiccustomattributes.all() + + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + + +class EpicRelatedUserStorySerializer(serializers.LightSerializer): + id = Field() + user_story = MethodField() + epic = MethodField() + order = Field() + + def get_user_story(self, obj): + return UserStorySerializer(obj.user_story).data + + def get_epic(self, obj): + return EpicSerializer(obj.epic).data diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index 334cd52d..75e3caad 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -25,7 +25,8 @@ from taiga.base.api.renderers import UnicodeJSONRenderer from taiga.base.utils.db import get_typename_for_model_instance from taiga.celery import app -from .serializers import (UserStorySerializer, IssueSerializer, TaskSerializer, +from .serializers import (EpicSerializer, EpicRelatedUserStorySerializer, + UserStorySerializer, IssueSerializer, TaskSerializer, WikiPageSerializer, MilestoneSerializer, HistoryEntrySerializer, UserSerializer) from .models import WebhookLog @@ -33,8 +34,11 @@ from .models import WebhookLog def _serialize(obj): content_type = get_typename_for_model_instance(obj) - - if content_type == "userstories.userstory": + if content_type == "epics.epic": + return EpicSerializer(obj).data + elif content_type == "epics.relateduserstory": + return EpicRelatedUserStorySerializer(obj).data + elif content_type == "userstories.userstory": return UserStorySerializer(obj).data elif content_type == "issues.issue": return IssueSerializer(obj).data diff --git a/tests/integration/test_webhooks_epics.py b/tests/integration/test_webhooks_epics.py new file mode 100644 index 00000000..f77dc829 --- /dev/null +++ b/tests/integration/test_webhooks_epics.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + +import pytest +from unittest.mock import patch + +from .. import factories as f + +from taiga.projects.history import services + + +pytestmark = pytest.mark.django_db(transaction=True) + + +def test_webhooks_when_create_epic(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_epic(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + obj.subject = "test webhook update" + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["subject"] == obj.subject + assert data["change"]["comment"] == "test_comment" + assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + + +def test_webhooks_when_delete_epic(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data + + +def test_webhooks_when_update_epic_attachments(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create attachments + attachment1 = f.EpicAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + attachment2 = f.EpicAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 2 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Update attachment + attachment1.description = "new attachment description" + attachment1.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 1 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 0 + + # Delete attachment + attachment2.delete() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["attachments"]["new"]) == 0 + assert len(data["change"]["diff"]["attachments"]["changed"]) == 0 + assert len(data["change"]["diff"]["attachments"]["deleted"]) == 1 + + +def test_webhooks_when_update_epic_custom_attributes(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + obj = f.EpicFactory.create(project=project) + + custom_attr_1 = f.EpicCustomAttributeFactory(project=obj.project) + ct1_id = "{}".format(custom_attr_1.id) + custom_attr_2 = f.EpicCustomAttributeFactory(project=obj.project) + ct2_id = "{}".format(custom_attr_2.id) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner) + assert send_request_mock.call_count == 2 + + # Create custom attributes + obj.custom_attributes_values.attributes_values = { + ct1_id: "test_1_updated", + ct2_id: "test_2_updated" + } + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 2 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Update custom attributes + obj.custom_attributes_values.attributes_values[ct1_id] = "test_2_updated" + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 1 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 0 + + # Delete custom attributes + del obj.custom_attributes_values.attributes_values[ct1_id] + obj.custom_attributes_values.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=obj.owner, comment="test_comment") + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "epic" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["change"]["comment"] == "test_comment" + assert len(data["change"]["diff"]["custom_attributes"]["new"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["changed"]) == 0 + assert len(data["change"]["diff"]["custom_attributes"]["deleted"]) == 1 + + +def test_webhooks_when_create_epic_related_userstory(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + epic = f.EpicFactory.create(project=project) + obj = f.RelatedUserStory.create(epic=epic) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "create" + assert data["type"] == "relateduserstory" + assert data["by"]["id"] == epic.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + + +def test_webhooks_when_update_epic_related_userstory(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + epic = f.EpicFactory.create(project=project) + obj = f.RelatedUserStory.create(epic=epic, order=33) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner) + assert send_request_mock.call_count == 2 + + obj.order = 66 + obj.save() + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "change" + assert data["type"] == "relateduserstory" + assert data["by"]["id"] == epic.owner.id + assert "date" in data + assert data["data"]["id"] == obj.id + assert data["data"]["order"] == obj.order + assert data["change"]["diff"]["order"]["to"] == 66 + assert data["change"]["diff"]["order"]["from"] == 33 + + +def test_webhooks_when_delete_epic_related_userstory(settings): + settings.WEBHOOKS_ENABLED = True + project = f.ProjectFactory() + f.WebhookFactory.create(project=project) + f.WebhookFactory.create(project=project) + + epic = f.EpicFactory.create(project=project) + obj = f.RelatedUserStory.create(epic=epic, order=33) + + with patch('taiga.webhooks.tasks._send_request') as send_request_mock: + services.take_snapshot(obj, user=epic.owner, delete=True) + assert send_request_mock.call_count == 2 + + (webhook_id, url, key, data) = send_request_mock.call_args[0] + assert data["action"] == "delete" + assert data["type"] == "relateduserstory" + assert data["by"]["id"] == obj.owner.id + assert "date" in data + assert "data" in data From a4c57a605bf845a55b5d6024f985dbee218f120c Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 15 Sep 2016 14:18:00 +0200 Subject: [PATCH 236/261] Fixing epic serialization --- taiga/projects/history/freeze_impl.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/taiga/projects/history/freeze_impl.py b/taiga/projects/history/freeze_impl.py index 6804c249..fd452257 100644 --- a/taiga/projects/history/freeze_impl.py +++ b/taiga/projects/history/freeze_impl.py @@ -190,18 +190,6 @@ def _generic_extract(obj:object, fields:list, default=None) -> dict: return result -@as_tuple -def extract_user_stories(obj) -> list: - for user_story in obj.user_stories.all(): - - yield {"id": user_story.id, - "ref": user_story.ref, - "subject": user_story.subject, - "project": { - "id": user_story.project.id, - "name": user_story.project.name, - "slug": user_story.project.slug}} - @as_tuple def extract_attachments(obj) -> list: for attach in obj.attachments.all(): @@ -313,8 +301,7 @@ def epic_freezer(epic) -> dict: "is_blocked": epic.is_blocked, "blocked_note": epic.blocked_note, "blocked_note_html": mdrender(epic.project, epic.blocked_note), - "custom_attributes": extract_epic_custom_attributes(epic), - "user_stories": extract_user_stories(epic), + "custom_attributes": extract_epic_custom_attributes(epic) } return snapshot @@ -326,7 +313,7 @@ def epic_related_userstory_freezer(related_us) -> dict: "epic": related_us.epic.id, "order": related_us.order } - + return snapshot From 3b5b658f3ca6284c87dc14ebbdf951d5e9e8c1e3 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 15 Sep 2016 14:20:40 +0200 Subject: [PATCH 237/261] Fixing gogs integration --- taiga/hooks/gogs/event_hooks.py | 2 +- tests/integration/test_hooks_gogs.py | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/taiga/hooks/gogs/event_hooks.py b/taiga/hooks/gogs/event_hooks.py index 8e68b8db..b392afe2 100644 --- a/taiga/hooks/gogs/event_hooks.py +++ b/taiga/hooks/gogs/event_hooks.py @@ -37,7 +37,7 @@ class PushEventHook(BaseGogsEventHook, BasePushEventHook): def get_data(self): result = [] commits = self.payload.get("commits", []) - project_url = self.payload.get("repository", {}).get("url", None) + project_url = self.payload.get("repository", {}).get("html_url", None) for commit in filter(None, commits): user_name = commit.get('author', {}).get('username', None) diff --git a/tests/integration/test_hooks_gogs.py b/tests/integration/test_hooks_gogs.py index 46c33dd9..11685d1a 100644 --- a/tests/integration/test_hooks_gogs.py +++ b/tests/integration/test_hooks_gogs.py @@ -105,7 +105,7 @@ def test_push_event_detected(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } @@ -140,7 +140,7 @@ def test_push_event_epic_processing(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -170,7 +170,7 @@ def test_push_event_issue_processing(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -200,7 +200,7 @@ def test_push_event_task_processing(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -230,7 +230,7 @@ def test_push_event_user_story_processing(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } @@ -261,7 +261,7 @@ def test_push_event_issue_mention(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -292,7 +292,7 @@ def test_push_event_task_mention(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -323,7 +323,7 @@ def test_push_event_user_story_mention(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } @@ -357,7 +357,7 @@ def test_push_event_multiple_actions(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -389,7 +389,7 @@ def test_push_event_processing_case_insensitive(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -415,7 +415,7 @@ def test_push_event_task_bad_processing_non_existing_ref(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } mail.outbox = [] @@ -443,7 +443,7 @@ def test_push_event_us_bad_processing_non_existing_status(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } @@ -472,7 +472,7 @@ def test_push_event_bad_processing_non_existing_status(client): } ], "repository": { - "url": "http://test-url/test/project" + "html_url": "http://test-url/test/project" } } @@ -511,7 +511,7 @@ def test_api_patch_project_modules(client): data = { "gogs": { "secret": "test_secret", - "url": "test_url", + "html_url": "test_url", } } response = client.patch(url, json.dumps(data), content_type="application/json") From b0931e8f88e99e61f52c67b16d0b75fd138fdd6f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 16 Sep 2016 12:03:12 +0200 Subject: [PATCH 238/261] Rewriting attach_user_stories_counts_to_queryset without json_build_object --- taiga/projects/epics/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/taiga/projects/epics/utils.py b/taiga/projects/epics/utils.py index 20f45046..49e394d8 100644 --- a/taiga/projects/epics/utils.py +++ b/taiga/projects/epics/utils.py @@ -41,9 +41,10 @@ def attach_extra_info(queryset, user=None, include_attachments=False): def attach_user_stories_counts_to_queryset(queryset, as_field="user_stories_counts"): model = queryset.model - sql = """SELECT json_build_object( - 'opened', COALESCE(SUM(CASE WHEN is_closed IS FALSE THEN 1 ELSE 0 END), 0), - 'closed', COALESCE(SUM(CASE WHEN is_closed IS TRUE THEN 1 ELSE 0 END), 0) + sql = """SELECT (SELECT row_to_json(t) + FROM (SELECT COALESCE(SUM(CASE WHEN is_closed IS FALSE THEN 1 ELSE 0 END), 0) AS "opened", + COALESCE(SUM(CASE WHEN is_closed IS TRUE THEN 1 ELSE 0 END), 0) AS "closed" + ) t ) FROM epics_relateduserstory INNER JOIN userstories_userstory ON epics_relateduserstory.user_story_id = userstories_userstory.id From 128da54eb18b8b3fa87d8c87e9ff70e56f98561d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 16 Sep 2016 12:26:59 +0200 Subject: [PATCH 239/261] Removing json_build_object postgresql calls --- taiga/projects/tasks/utils.py | 9 ++++++--- taiga/projects/userstories/utils.py | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/taiga/projects/tasks/utils.py b/taiga/projects/tasks/utils.py index d5775d19..0d8661fc 100644 --- a/taiga/projects/tasks/utils.py +++ b/taiga/projects/tasks/utils.py @@ -44,9 +44,12 @@ def attach_user_story_extra_info(queryset, as_field="user_story_extra_info"): "epics_epic"."ref" AS "ref", "epics_epic"."subject" AS "subject", "epics_epic"."color" AS "color", - json_build_object('id', "projects_project"."id", - 'name', "projects_project"."name", - 'slug', "projects_project"."slug") AS "project" + (SELECT row_to_json(p) + FROM (SELECT "projects_project"."id" AS "id", + "projects_project"."name" AS "name", + "projects_project"."slug" AS "slug" + ) p + ) AS "project" FROM "epics_relateduserstory" INNER JOIN "epics_epic" ON "epics_epic"."id" = "epics_relateduserstory"."epic_id" diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index ef5e8ba0..57e4ecd3 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -114,9 +114,12 @@ def attach_epics(queryset, as_field="epics_attr"): "epics_epic"."ref" AS "ref", "epics_epic"."subject" AS "subject", "epics_epic"."color" AS "color", - json_build_object('id', "projects_project"."id", - 'name', "projects_project"."name", - 'slug', "projects_project"."slug") AS "project" + (SELECT row_to_json(p) + FROM (SELECT "projects_project"."id" AS "id", + "projects_project"."name" AS "name", + "projects_project"."slug" AS "slug" + ) p + ) AS "project" FROM "epics_relateduserstory" INNER JOIN "epics_epic" ON "epics_epic"."id" = "epics_relateduserstory"."epic_id" INNER JOIN "projects_project" ON "projects_project"."id" = "epics_epic"."project_id" From 195880767a06c91304762dc796793e9934f5fe60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 19 Sep 2016 18:28:19 +0200 Subject: [PATCH 240/261] Enable epics in all sample data projects and not created custom fields in empty projects --- .../management/commands/sample_data.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/taiga/projects/management/commands/sample_data.py b/taiga/projects/management/commands/sample_data.py index 9a4d2b22..e8b47290 100644 --- a/taiga/projects/management/commands/sample_data.py +++ b/taiga/projects/management/commands/sample_data.py @@ -191,38 +191,38 @@ class Command(BaseCommand): if role.computable: computable_project_roles.add(role) - # added custom attributes - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - EpicCustomAttribute.objects.create(name=name, - description=self.sd.words(3, 12), - type=self.sd.choice(TYPES_CHOICES)[0], - project=project, - order=i) - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - UserStoryCustomAttribute.objects.create(name=name, + # If the project isn't empty + if x not in empty_projects_range: + # added custom attributes + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + EpicCustomAttribute.objects.create(name=name, + description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], + project=project, + order=i) + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + UserStoryCustomAttribute.objects.create(name=name, + description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], + project=project, + order=i) + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + TaskCustomAttribute.objects.create(name=name, + description=self.sd.words(3, 12), + type=self.sd.choice(TYPES_CHOICES)[0], + project=project, + order=i) + names = set([self.sd.words(1, 3) for i in range(1, 6)]) + for name in names: + IssueCustomAttribute.objects.create(name=name, description=self.sd.words(3, 12), type=self.sd.choice(TYPES_CHOICES)[0], project=project, order=i) - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - TaskCustomAttribute.objects.create(name=name, - description=self.sd.words(3, 12), - type=self.sd.choice(TYPES_CHOICES)[0], - project=project, - order=i) - names = set([self.sd.words(1, 3) for i in range(1, 6)]) - for name in names: - IssueCustomAttribute.objects.create(name=name, - description=self.sd.words(3, 12), - type=self.sd.choice(TYPES_CHOICES)[0], - project=project, - order=i) - # If the project isn't empty - if x not in empty_projects_range: start_date = now() - datetime.timedelta(55) # create milestones @@ -269,8 +269,6 @@ class Command(BaseCommand): for y in range(self.sd.int(*NUM_EPICS)): epic = self.create_epic(project) - - project.refresh_from_db() # Set color for some tags: @@ -588,6 +586,7 @@ class Command(BaseCommand): blocked_code=blocked_code) project.is_kanban_activated = True + project.is_epics_activated = True project.save() take_snapshot(project, user=project.owner) From aff6f56cc0ba47428b6a83b5f0b6a44888e4c1b1 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 20 Sep 2016 07:47:16 +0200 Subject: [PATCH 241/261] Improving load_dump command --- taiga/export_import/management/commands/load_dump.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index 8a4ca585..b04a1602 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -65,6 +65,10 @@ class Command(BaseCommand): except Project.DoesNotExist: pass signals.post_delete.receivers = receivers_back + else: + slug = data.get('slug', None) + if slug is not None and Project.objects.filter(slug=slug).exists(): + del data['slug'] user = User.objects.get(email=owner_email) services.store_project_from_dict(data, user) From d3ede7fda2efc0860c20be225aad8f3a3c6f857a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 20 Sep 2016 10:39:02 +0200 Subject: [PATCH 242/261] Removing atomic transaction from load_dump command --- .../management/commands/load_dump.py | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index b04a1602..c01f577f 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -50,28 +50,27 @@ class Command(BaseCommand): data = json.loads(open(dump_file_path, 'r').read()) try: - with transaction.atomic(): - if overwrite: - receivers_back = signals.post_delete.receivers - signals.post_delete.receivers = [] - try: - proj = Project.objects.get(slug=data.get("slug", "not a slug")) - proj.tasks.all().delete() - proj.user_stories.all().delete() - proj.issues.all().delete() - proj.memberships.all().delete() - proj.roles.all().delete() - proj.delete() - except Project.DoesNotExist: - pass - signals.post_delete.receivers = receivers_back - else: - slug = data.get('slug', None) - if slug is not None and Project.objects.filter(slug=slug).exists(): - del data['slug'] + if overwrite: + receivers_back = signals.post_delete.receivers + signals.post_delete.receivers = [] + try: + proj = Project.objects.get(slug=data.get("slug", "not a slug")) + proj.tasks.all().delete() + proj.user_stories.all().delete() + proj.issues.all().delete() + proj.memberships.all().delete() + proj.roles.all().delete() + proj.delete() + except Project.DoesNotExist: + pass + signals.post_delete.receivers = receivers_back + else: + slug = data.get('slug', None) + if slug is not None and Project.objects.filter(slug=slug).exists(): + del data['slug'] - user = User.objects.get(email=owner_email) - services.store_project_from_dict(data, user) + user = User.objects.get(email=owner_email) + services.store_project_from_dict(data, user) except err.TaigaImportError as e: if e.project: e.project.delete_related_content() From ce0fcbaa2f231369e4a42693d56d0e191b7063e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 20 Sep 2016 13:22:20 +0200 Subject: [PATCH 243/261] [i18n] Update locales --- taiga/locale/ca/LC_MESSAGES/django.po | 1835 +++++++++++-------- taiga/locale/de/LC_MESSAGES/django.po | 1893 ++++++++++--------- taiga/locale/en/LC_MESSAGES/django.po | 215 +-- taiga/locale/es/LC_MESSAGES/django.po | 1892 ++++++++++--------- taiga/locale/fi/LC_MESSAGES/django.po | 1871 ++++++++++--------- taiga/locale/fr/LC_MESSAGES/django.po | 1862 ++++++++++--------- taiga/locale/it/LC_MESSAGES/django.po | 1919 +++++++++++--------- taiga/locale/nl/LC_MESSAGES/django.po | 1852 ++++++++++--------- taiga/locale/pl/LC_MESSAGES/django.po | 1894 ++++++++++--------- taiga/locale/pt_BR/LC_MESSAGES/django.po | 1898 ++++++++++--------- taiga/locale/ru/LC_MESSAGES/django.po | 1901 ++++++++++--------- taiga/locale/sv/LC_MESSAGES/django.po | 1858 ++++++++++--------- taiga/locale/tr/LC_MESSAGES/django.po | 1856 ++++++++++--------- taiga/locale/zh-Hant/LC_MESSAGES/django.po | 1897 ++++++++++--------- 14 files changed, 13681 insertions(+), 10962 deletions(-) diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po index d643b4f8..2759a15e 100644 --- a/taiga/locale/ca/LC_MESSAGES/django.po +++ b/taiga/locale/ca/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ca/)\n" @@ -20,150 +20,154 @@ msgstr "" "Language: ca\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "El registre públic està deshabilitat" -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "Sistema de registre invàlid" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "Sistema de login invàlid" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "El mot d'usuari ja està en ús." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Aquest e-mail ja està en ús." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "El token no s'ajusta a cap invitació vàlida" + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Aquest usuari ja està registrat" + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Error creant un nou usuari." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Token invàlid" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "nom d'usuari invàlid" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Requerit. 255 caràcters o menys. Lletres, nombres i caràcters /./-/_" -#: 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:94 -msgid "Token not matches any valid invitation." -msgstr "El token no s'ajusta a cap invitació vàlida" - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Aquest usuari ja està registrat" - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Error creant un nou usuari." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Token invàlid" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Aquest camp es obligatori" -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Valor invàlid" -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "'%s' valor deu ser Verdader o Fals" -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "Introdueix un 'slug' vàlid: lletres, nombres, barra baixa o guió." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Selecciona una opció vàlida. %(value)s no es una opció vàlida." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Introdueix una adreça de correu vàlida-" -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "La data te un format erroni. Utilitza un del següents formats: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "La data te un format erroni. Utilitza un del següents formats: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "L'hora te un format erroni. Utilitza un del següents formats: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Introdueix un nombre complet." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Asegurat que aquest valor es inferior i igual a %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Asegurat que aquest valor es superior o igual a %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" deu ser un float." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Introdueix un nombre." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Asegurat que no hi ha més de %s digits en total." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Asegurat que no hi ha més de %s posicions decimals." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Asegurat que no hi ha més de %s dígits abans del decimal." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Cap fitxer enviat. Comprova el tipus de codificació en el formulari." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Cap fitxer enviat." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "El fitxer enviat està buit." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -171,11 +175,11 @@ msgstr "" "Asegurat que el nom del fitxer te un màxim de %(max)d caràcters (te " "%(length)d)" -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "Per favor envia un fitxer o cancela el checkbox, pero no ambdós." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -183,180 +187,177 @@ msgstr "" "Puja una imatge vàlida. El fitxer que has pujat no ès una imatge o el fitxer " "està corrupte." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 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/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Pàgina invàlida (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "" -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "" -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "" -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "" -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "" -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "" -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "" -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Error de connexió." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "" -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "" -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "" -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "" -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "" -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "" -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "" -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "" -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Error inesperat" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "No s'ha trobat." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Mètode no suportat per aquest endpoint." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Arguments invàlids." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Validació de data errònia" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Error d'integritat per argument invàlid o erroni." -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Precondició errònia." -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "" -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "tags" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -411,7 +412,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -468,103 +469,88 @@ msgstr "" " Comentari: %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Es necessita arxiu dump." -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Format d'arxiu dump invàlid" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "" - -#: taiga/export_import/serializers.py:443 -#: 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:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Conté camps personalitzats invàlids." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -584,15 +570,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -743,77 +729,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] El teu bolcat de dades ha sigut importat" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Contingut invàlid. Deu ser {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Conté camps personalitzats invàlids." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "Nom" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "Descripció" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "Nom complet" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "Adreça d'email" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "Comentari" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "Data de creació" @@ -844,7 +850,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Informació extra" @@ -878,504 +884,577 @@ msgstr "" "\n" "[Taiga] Feedback de %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "El payload no és un arxiu json vàlid" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "El projecte no existeix" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Firma no vàlida." -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: 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:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "L'estatus no existeix." - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Informació d'incidència no vàlida." - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Informació del comentari a l'incidència no vàlid." -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." +msgstr "" + +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Informació d'incidència no vàlida." + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 +#, python-brace-format +msgid "" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"Changed status from {platform} commit.\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "L'element referenciat no existeix" -#: taiga/hooks/github/event_hooks.py:201 -#, python-brace-format -msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "L'estatus no existeix." -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -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/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Veure projecte" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Veure fita" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Veure història d'usuari" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Veure tasca" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Veure incidència" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Veure pàgina del wiki" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Veure links del wiki" -#: 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:75 -msgid "Add wiki page" -msgstr "Afegeix pàgina del wiki" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Modifica pàgina del wiki" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Afegeix enllaç de wiki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modifica enllaç de wiki" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Afegeix fita" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modifica fita" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Borra fita" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Veure història d'usuari" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Afegeix història d'usuari" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modifica història d'usuari" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Borra història d'usuari" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Afegeix tasca" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modifica tasca" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Borra tasca" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Afegeix incidència" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modifica incidència" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Borra incidència" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Afegeix pàgina del wiki" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Modifica pàgina del wiki" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Borra pàgina de wiki" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Afegeix enllaç de wiki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modifica enllaç de wiki" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Borra enllaç de wiki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Modifica projecte" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Afegeix membre" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Borra membre" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Borra projecte" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Afegeix membre" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Borra membre" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Administrar valors de projecte" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Administrar rols" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "Amo" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Arguments incomplets." -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Format d'image invàlid" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "No tens permisos per a veure açò." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "Projecte" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "Tipus de contingut" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "Id d'objecte" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "Data de modificació" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "Arxiu adjunt" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "està obsolet " -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "Ordre" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 -msgid "Text" +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:29 -msgid "Date" +msgid "Multi-Line Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:30 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "tipus" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "història d'usuari" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "tasca" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "incidéncia" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Ja existix altre amb el matex nom." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "estatus" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "tema" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "color" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "assignada a" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "requeriment de client" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "requeriment d'equip" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Canvia" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Crea" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Borra" @@ -1431,7 +1510,7 @@ msgstr "Borrat" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Sense assignar" @@ -1478,95 +1557,75 @@ msgstr "Desde:" msgid "To:" msgstr "A:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "contingut" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "nota de bloqueig" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 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:162 +#: taiga/projects/issues/api.py:160 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:166 +#: taiga/projects/issues/api.py:164 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:170 +#: taiga/projects/issues/api.py:168 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:174 +#: taiga/projects/issues/api.py:172 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:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "estatus" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "severitat" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "prioritat" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "fita" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "Data de finalització" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "tema" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "assignada a" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "referència externa" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "M'agrada" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Fans" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1578,8 +1637,9 @@ msgstr "Data estimada d'inici" msgid "estimated finish date" msgstr "Data estimada de finalització" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "està tancat" @@ -1591,120 +1651,132 @@ msgstr "disponibilitat" 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 "No hi ha cap sprint amb aquest id" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "està bloquejat" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "email" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "text extra d'invitació" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "L'usuari ja es membre del projecte" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "Points per defecte" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "estatus d'història d'usuai per defecte" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "Points per defecte" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "Estatus de tasca per defecte" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "Prioritat per defecte" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "Severitat per defecte" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "Status d'incidència per defecte" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "Tipus d'incidència per defecte" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "membres" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "total de fites" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "total de punts d'història" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "activa panell de backlog" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "activa panell de kanban" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "activa panell de wiki" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "activa panell d'incidències" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "sistema de videoconferència" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "template de creació" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "es privat" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "permisos d'anònims" @@ -1712,169 +1784,251 @@ msgstr "permisos d'anònims" msgid "user permissions" msgstr "permisos d'usuaris" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "es privat" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "colors de tags" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "Actualitzada data" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "configuració de mòdules" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "està arxivat" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "color" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "limit de treball en progrés" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "valor" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "rol d'amo per defecte" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "opcions per defecte" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "status d'històries d'usuari" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "punts" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "status de tasques" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "status d'incidències" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "tipus d'incidències" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "prioritats" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "severitats" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "rols" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "creada data" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2352,141 +2506,135 @@ msgstr "" "\n" "[%(project)s] Borrada pàgina de Wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "Versió" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Aquest e-mail ja està en ús" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Rol invàlid per al projecte" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Opcions per defecte" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Estatus d'històries d'usuari" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Punts" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Estatus de tasques" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Estatus d'incidéncies" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Tipus d'incidéncies" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Prioritats" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Severitats" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Rols" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Token invàlid" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "tags" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "colors de tags" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2502,9 +2650,35 @@ msgstr "ordre de taskboard" 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/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -2863,12 +3037,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -2879,12 +3053,12 @@ msgid "" msgstr "" #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -2893,303 +3067,388 @@ msgid "" msgstr "" #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "ordre de backlog" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "ordre d'sprint" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "data de finalització" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "requeriment de client" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "requeriment d'equip" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "generat desde incidéncia" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 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:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "No hi ha cap projecte amb eixe id" -#: taiga/projects/validators.py:38 -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:142 +msgid "Email address is already taken" +msgstr "Aquest e-mail ja està en ús" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "No hi ha cap estatus de tasca amb eixe id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Rol invàlid per al projecte" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Opcions per defecte" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Estatus d'històries d'usuari" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Punts" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Estatus de tasques" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Estatus d'incidéncies" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Tipus d'incidéncies" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioritats" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Severitats" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Rols" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Vots" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Vot" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "últim a modificar" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3217,53 +3476,53 @@ msgstr "" msgid "Important dates" msgstr "Dates importants" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Email duplicat" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Email no vàlid" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Nom d'usuari o email invàlid" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Correu enviat satisfactòriament" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Paràmetre de password actual requerit" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Paràmetre de password requerit" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Password invàlid, al menys 6 caràcters requerits" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Password actual invàlid" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 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:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Invàlid. Estás segur que el token es correcte?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "estatus de superusuari" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3271,24 +3530,24 @@ msgstr "" "Designa que aquest usuari te tots els permisos sense asignarli-los " "explícitament." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "mot d'usuari" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 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:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Introdueix un nom d'usuari vàlid" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "actiu" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3296,71 +3555,63 @@ msgstr "" "Designa si aquest usuari ha de se tractac com actiu. Deselecciona açó en " "lloc de borrar el compte." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "foto" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "data d'unió" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "llenguatge per defecte" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "zona horaria per defecte" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "coloritza tags" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "token de correu" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "nova adreça de correu" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "permissos" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "invàlid" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Nom d'usuari invàlid" - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "" @@ -3481,47 +3732,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "invàlid" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Nom d'usuari invàlid" + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "" diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po index f5556358..f69edccb 100644 --- a/taiga/locale/de/LC_MESSAGES/django.po +++ b/taiga/locale/de/LC_MESSAGES/django.po @@ -20,9 +20,9 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-06-24 07:58+0000\n" -"Last-Translator: Torsten Karge \n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" +"Last-Translator: Taiga Dev Team \n" "Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/de/)\n" "MIME-Version: 1.0\n" @@ -31,170 +31,174 @@ msgstr "" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Die Registrierung ist für die Öffentlichkeit gesperrrt." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "Ungültige Registrierungsart" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "Ungültige Loginart" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Der Benutzername wird schon verwendet." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Diese E-Mail Adresse wird schon verwendet." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Der Benutzer ist schon registriert." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Dieser Benutzer ist schon ein Mitglied des Projektes." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Fehler bei der Erstellung des neuen Benutzers." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Ungültiges Token" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "Ungültiger Benutzername" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "255 oder weniger Zeichen aus Buchstaben, Zahlen und Punkt, Minus oder " "Unterstrich erforderlich." -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Der Benutzername wird schon verwendet." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "Diese E-Mail Adresse wird schon verwendet." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Das Token kann keiner gültigen Einladung zugeordnet werden." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Der Benutzer ist schon registriert." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "Dieser Benutzer ist schon ein Mitglied des Projektes." - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Fehler bei der Erstellung des neuen Benutzers." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Ungültiges Token" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Das ist ein Pflichtfeld." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Ungültiger Wert." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "Der Wert für '%s' muss entweder True/Wahr oder False/Falsch sein." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Geben Sie einen gültigen 'slug' ein, bestehend aus Buchstaben, Zahlen, " "Unterstrichen oder Bindestrichen." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Bitte machen Sie eine gültige Auswahl. %(value)s ist nicht verfügbar." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Geben Sie bitte eine gültige E-Mail Adresse an." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "" "Das Datum hat das falsche Format. Bitte verwenden Sie eines der folgenden " "Formate: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "" "Der Datentyp 'Datetime' hat ein falsches Format. Bitte verwenden Sie eines " "der folgenden Formate: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "" "Die Zeit hat ein falsches Format. Bitte verwenden Sie eines der folgenden " "Formate: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Geben Sie bitte eine ganze Zahl ein." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" "Stellen Sie sicher, dass dieser Wert niedriger oder gleich ist wie " "%(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" "Stellen Sie sicher, dass dieser Wert höher oder gleich ist wie " "%(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "Der Wert für '%s' muss eine Fließkommazahl sein." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Bitte geben Sie eine Zahl ein." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "" "Bitte stellen Sie sicher, dass nicht mehr als %s insgesamt vorhanden sind. " -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "" "Bitte stellen Sie sicher, dass nicht mehr als %s Dezimalstellen vorhanden " "sind." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "" "Stellen Sie sicher, dass nicht mehr als %s Ziffern vor dem Dezimalpunkt " "vorhanden sind." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "Es wurde keine Datei übergeben. Prüfen Sie die Kodierung der HTML-Form." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Es wurde keine Datei eingereicht." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Die eingereichte Datei ist leer." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -202,13 +206,13 @@ msgstr "" "Stellen Sie sicher, dass dieser Dateiname höchstens %(max)d Zeichen hat (er " "hat %(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Bitte senden Sie entweder eine Datei oder markieren Sie \"Löschen\", nicht " "beides." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -216,182 +220,179 @@ msgstr "" "Bitte laden Sie ein gültiges Bild hoch. Die Datei, die Sie hochgeladen " "haben, ist entweder kein Bild oder defekt." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Blockiertes Element" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Seite ist nicht 'letzte', noch kann diese konvertiert werden." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Ungültige Seite (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Ungültige Berechtigungsdefinition" -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Ungültige pk '%s' - Das Objekt existiert nicht." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Falsche Eingabe. Erwartet pk Wert, erhalten %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Objekt mit %s=%s existiert nicht." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Ungültiger Hyperlink - keine passende URL. " -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Ungültiger Hyperlink - Falsche URL Verknüpfung" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Ungültiger Hyperlink durch Konfigurationsfehler" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Ungültiger Hyperlink - Ziel existiert nicht." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Falsche Eingabe. Erwartet url Zeichenkette, erhalten %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Ungültige Daten" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Es gab keine Eingabe" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Es können nur existierende Einträge aktualisiert werden. Eine Neuerstellung " "ist nicht möglich." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Es wurde eine Liste von Einträgen erwartet." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Nicht gefunden." -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Zugriff verweigert" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Fehler bei der Serveranmeldung" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Verbindungsfehler." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Fehlerhafte Anfrage." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Ungültige Authentifizierungsdaten." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Die Authentifizierungsdaten wurden nicht erbracht." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Sie haben keine Berechtigung, diese Aktion auszuführen. " -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Methode '%s' ist nicht erlaubt." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Könnte der Anforderung im Header nicht entsprechen." -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Nicht unterstützter Medientyp '%s' in Anfrage." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Die Anfrage wurde ausgebremst." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Voraussichtlich verfügbar in %d second%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Unerwarteter Fehler" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Nicht gefunden." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Methode wird für diesen Endpunkt nicht unterstützt. " -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Falsche Argumente" -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Fehler bei Datenüberprüfung " -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Integritätsfehler wegen falscher oder ungültiger Argumente" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Voraussetzungsfehler" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "Kein Raum für weitere Projekte." -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Fehler in Filter Parameter Typen." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' muss ein Integer-Wert sein." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "Tags" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -446,7 +447,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -458,22 +459,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" Taiga Support:\n" -" " -"%(support_url)s\n" -"
\n" -" Kontaktieren Sie uns:\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Mailing list:\n" -" \n" -" %(mailing_list_url)s\n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -526,103 +511,88 @@ msgstr "" "Kommentar: %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Es ist mindestens eine Rolle nötig" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Exportdatei erforderlich" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Ungültiges Exportdatei Format" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Enthält ungültige Benutzerfelder." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Der Name für das Projekt ist doppelt vergeben" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "Fehler beim Importieren der Projektdaten" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "Fehler beim Importieren der Rollen" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "Fehler beim Importieren der Mitgliedschaften" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "Fehler beim Importieren der Listen von Projektattributen" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte " -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "Fehler beim Importieren der Kundenattribute" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "Fehler beim Import der Sprints" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "Fehler beim Importieren der User-Stories" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "Fehler beim Importieren der Aufgaben" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "Fehler beim Importieren der Tickets" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "Fehler beim Importieren der User-Stories" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "Fehler beim Importieren der Aufgaben" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "Fehler beim Importieren von Wiki Seiten" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "Fehler beim Importieren von Wiki Links" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "Fehler beim Importieren der Schlagworte" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "Fehler beim Importieren der Chroniken" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "unerwarteter Fehler beim Projekt-Import" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Fehler beim Erzeugen der Projekt Export-Datei " -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -657,15 +627,15 @@ msgstr "" "FEHLER-PFAD:\n" "------------" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Fehler beim Laden von Projekt Export-Datei" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "Fehler beim Laden Ihrer Projekt-Dump-Datei" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "-- keine detaillierten Infos --" @@ -909,77 +879,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Ihre Projekt Export-Datei wurde importiert" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" wurde in diesem Projekt nicht gefunden" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Invalider Inhalt. Er muss wie folgt sein: {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Enthält ungültige Benutzerfelder." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Der Name für das Projekt ist doppelt vergeben" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Authentifizierung erforderlich" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "Name" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "Icon URL" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "Web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "Beschreibung" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Nächste URL" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "Geheimer Schlüssel für Verschlüsselung der Anwensungs-Token" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "Benutzer" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "Applikation" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "vollständiger Name" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "E-Mail Adresse" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "Kommentar" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "Erstellungsdatum" @@ -1009,7 +999,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Zusätzliche Information" @@ -1044,389 +1034,345 @@ msgstr "" "[Taiga] Feedback von %(full_name)s <%(email)s>\n" " \n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "Die Nutzlast ist kein gültiges json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Das Projekt existiert nicht" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Falsche Signatur" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "Das referenzierte Element existiert nicht" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Der Status existiert nicht" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Der Status des BitBucket Commits hat sich geändert" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Ungültige Ticket-Information" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"Problem-Bericht erstellt von [@{bitbucket_user_name}]({bitbucket_user_url} " -"\"Schau @{bitbucket_user_name}'s BitBucket profile\") von BitBucket.\n" -"Original BitBucket Problem-Bericht: [bb#{number} - {subject}]" -"({bitbucket_url} \"Gehe zu 'bb#{number} - {subject}'\"):\n" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" "\n" -"{description}" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Ticket erstellt von BitBucket." - -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Ungültige Ticket-Kommentar Information" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -"Kommentar von [@{bitbucket_user_name}]({bitbucket_user_url} \"Schau " -"@{bitbucket_user_name}'s BitBucket profile\") von BitBucket.\n" -"Original BitBucket Problem-Bericht: [bb#{number} - {subject}]" -"({bitbucket_url} \"Gehe zu 'bb#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Ungültige Ticket-Information" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Kommentar von BitBucket\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Status geändert von [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Der Status des GitHub Commits hat sich geändert" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Ticket erstellt von [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -" Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -" {description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Ticket erstellt von GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"Kommentar von [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") von GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Kommentar von GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Das referenzierte Element existiert nicht" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Der Status des GitLab Commits hat sich geändert" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Der Status existiert nicht" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Erstellt von GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Kommentar von [@{gitlab_user_name}]({gitlab_user_url} \"Schau " -"@{gitlab_user_name}'s GitLab profile\") von GitLab.\n" -"Original GitLab Problem-Bericht: [gl#{number} - {subject}]({gitlab_url} " -"\"Gehe zu 'gl#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Kommentar von GitLab:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Projekt ansehen" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Meilensteine ansehen" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "User-Stories ansehen. " -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Aufgaben ansehen" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Tickets ansehen" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Wiki Seiten ansehen" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Wiki Links ansehen" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Mitgliedschaft beantragen" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "User-Story zu Projekt hinzufügen" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Kommentar zu User-Stories hinzufügen" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Kommentare zu Aufgaben hinzufügen" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Tickets hinzufügen" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Kommentare zu Tickets hinzufügen" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Wiki Seite hinzufügen" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Wiki Seite ändern" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Wiki Link hinzufügen" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Wiki Link ändern" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Meilenstein hinzufügen" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Meilenstein ändern" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Meilenstein löschen" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "User-Story ansehen" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "User-Story hinzufügen" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "User-Story ändern" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "User-Story löschen" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Aufgabe hinzufügen" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Aufgabe ändern" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Aufgabe löschen" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Ticket hinzufügen" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Ticket ändern" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Gelöschtes Ticket" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Wiki Seite hinzufügen" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Wiki Seite ändern" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Wiki Seite löschen" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Wiki Link hinzufügen" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Wiki Link ändern" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Wiki Link löschen" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Projekt ändern" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Mitglied hinzufügen" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Mitglied entfernen" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Projekt löschen" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Mitglied hinzufügen" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Mitglied entfernen" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Administrator Projekt Werte" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Administrator-Rollen" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "Besitzer" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Unvollständige Argumente" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Ungültiges Bildformat" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Unglültiger Templatename" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Ungültige Templatebeschreibung" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "Ungültige Benutzer-Id" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "Der Benutzer existiert nicht" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "Der Benutzer muss bereits Mitglied des Projektes sein" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" @@ -1434,158 +1380,233 @@ msgstr "" "Das Projekt muss einen Eigentümer haben und mindestens ein Benutzer muss ein " "aktiver Administrator sein" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Sie haben keine Berechtigungen für diese Ansicht" -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Teil-Aktualisierungen sind nicht unterstützt" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "Projekt" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "Inhaltsart" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "Objekt Nr." -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "Zeitpunkt der Änderung" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "Angehangene Datei" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "SHA1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "wurde verworfen" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "Reihenfolge" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "Erscheint in" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Kunde" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Gesprächig" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "Dieses Projekt ist durch den Administrator blockiert" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "Dieses Projekt ist blockiert, weil es der Eigentümer verlassen hat." -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Text" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Mehrzeiliger Text" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Datum" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "Art" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "Werte" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "User-Story" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "Aufgabe" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "Ticket" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Dieser Name wird schon verwendet." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "Status" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "Betreff" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "Farbe" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "zugewiesen an" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "ist Kundenanforderung" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "ist Teamanforderung" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Kommentar bereits gelöscht" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Kommentar nicht gelöscht" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Ändern" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Erzeugen" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Löschen" @@ -1641,7 +1662,7 @@ msgstr "entfernt" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Nicht zugewiesen" @@ -1688,99 +1709,79 @@ msgstr "Von:" msgid "To:" msgstr "An:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "Inhalt" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "Blockierungsgrund" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "Sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diesen Sprint zu setzen." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diesen Status zu setzen. " -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diese Gewichtung zu setzen." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "" "Sie haben nicht die Berechtigung, das Ticket auf diese Priorität zu setzen. " -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Sie haben nicht die Berechtigung, das Ticket auf diese Art zu setzen." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "Status" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "Gewichtung" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "Priorität" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "Meilenstein" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "Datum der Fertigstellung" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "Betreff" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "zugewiesen an" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "externe Referenz" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Like" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Likes" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "Slug" @@ -1792,8 +1793,9 @@ msgstr "geschätzter Starttermin" msgid "estimated finish date" msgstr "geschätzter Endtermin" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "ist geschlossen" @@ -1805,120 +1807,132 @@ msgstr "Verfügbarkeit" msgid "The estimated start must be previous to the estimated finish." msgstr "Der erwartete Beginn muss vor dem erwarteten Ende liegen. " -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Es gibt keinen Sprint mit dieser id" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "wird blockiert" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' Parameter ist ein Pflichtfeld" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "Der 'project' Parameter ist ein Pflichtfeld" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "E-Mail" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "erstellt am " -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "Token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "Einladung Zusatztext " -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "Benutzerreihenfolge" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "Der Benutzer ist bereits Mitglied dieses Projekts" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "voreingestellte Punkte" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "voreingesteller User-Story Status " -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "voreingestellte Punkte" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "voreingestellter Aufgabenstatus" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "voreingestellte Priorität " -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "voreingestellte Gewichtung " -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "voreingestellter Ticket Status" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "voreingestellter Ticket Typ" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "Logo" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "Mitglieder" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "Meilensteine Gesamt" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "Story Punkte insgesamt" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "aktives Backlog Panel" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "aktives Kanban Panel" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "aktives Wiki Panel" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "aktives Tickets Panel" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "Videokonferenzsystem" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "Zusatzdaten Videokonferenz" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "Vorlage erstellen" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "ist privat" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "Rechte für anonyme Nutzer" @@ -1926,169 +1940,251 @@ msgstr "Rechte für anonyme Nutzer" msgid "user permissions" msgstr "Rechte für registrierte Nutzer" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "ist privat" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "ist gekennzeichnet" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "sucht nach Mitarbeitern" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "Hinweis für Mitarbeitersuche" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "Tag Farben" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "Projekt-Transfer-Token" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "Blockierter Code" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "Aktualisierungsdatum" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "Count" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "Unterstützer letzte Woche" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "Unterstützer letzten Monat" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "Unterstützer letztes Jahr" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "Aktivitäten letzte Woche" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "Aktivitäten letzten Monat" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "Aktivitäten letztes Jahr" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "Module konfigurieren" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "ist archiviert" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "Farbe" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "Ausführungslimit" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "Wert" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "voreingestellte Besitzerrolle" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "Vorgabe Optionen" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "User-Story Status " -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "Punkte" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "Aufgaben Status" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "Ticket Status" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "Ticket Arten" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "Prioritäten" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "Gewichtung" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "Rollen" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Beteiligt" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Alle" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Keine" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "Erstelldatum" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "Chronik Einträge" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "Benutzer benachrichtigen" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Beobachtet" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Benachrichtigung für bestimmte Benutzer und Projekt aktiviert" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Ungültiger Wert für Benachrichtigungslevel" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2844,151 +2940,144 @@ msgstr "" "[%(project)s] löschte die Wiki Seite \"%(page)s\"\n" "\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Beobachter enthält ungültige Benutzer " -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "Die Watcher beinhalten einen ungültigen Benutzer" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "Der Versionsparameter ist ungültig" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Die Version stimmt nicht mit der aktuellen überein" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "Version" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" "Sie können das Projekt nicht verlassen, wenn Sie der Eigentümer sind oder " "wenn keine weiteren Administratoren vorhanden sind." -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Die E-Mailadresse ist bereits vergeben" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Ungültige Rolle für dieses Projekt" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." -msgstr "Der Projekteigentümer muss Administrator sein." - -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -"Mindestens ein Benutzer muss ein aktiver Administrator des Projektes sein." -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Voreingestellte Optionen" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Status für User-Stories" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Punkte" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Aufgaben Status" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Ticket Status" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Ticket Arten" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Prioritäten" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Gewichtung" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Rollen" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" "Sie haben Ihr aktuelles Limit für die Mitgliederanzahl für private Projekte " "erreicht" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" "Sie haben Ihr aktuelles Limit für die Mitgliederanzahl für öffentliche " "Projekte erreicht" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "Sie können nicht mehr private Projekte haben" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "Sie können nicht mehr öffentliche Projekte haben." -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Zukünftiger Sprint" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Projektende" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Token ist ungültig" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "Token ist abgelaufen" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "Tags" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "Tag Farben" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "" "Sie haben nicht die Berechtigung, diesen Sprint auf diese Aufgabe zu setzen" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" "Sie haben nicht die Berechtigung, diese User-Story auf diese Aufgabe zu " "setzen" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "" "Sie haben nicht die Berechtigung, diesen Status auf diese Aufgabe zu setzen." @@ -3005,9 +3094,35 @@ msgstr "Taskboard Befehl " msgid "is iocaine" msgstr "ist Iocaine" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Es gibt keine Aufgabe mit dieser id" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3402,12 +3517,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3424,12 +3539,12 @@ msgstr "" "seiner Kunden gerecht wird." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3443,307 +3558,393 @@ msgstr "" "der nächsten Integrationsstufe verbaut werden." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Neu" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Fertig" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "In Arbeit" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Bereit zum Testen" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Erledigt" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Archiviert" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Geschlossen" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Information wird benötigt" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Verschoben" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Zurückgewiesen" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Fehler" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Frage" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Erweiterung" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Niedrig" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Hoch" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Wunschliste" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Gering" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Wichtig" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Kritisch" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Projekteigentümer " #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Sie haben nicht die Berechtigung, diesen Sprint auf diese User-Story zu " "setzen." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "Sie haben nicht die Berechtigung, diesen Status auf diese User-Story zu " "setzen." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Erstelle die User-Story #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "Rolle" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "Backlog Befehl " -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "Sprintreihenfolge" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "Endtermin" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "ist Kundenanforderung" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "ist Teamanforderung" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "erzeugt von Ticket" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Es gibt keine User-Story mit dieser id" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Es gibt kein Projekt mit dieser id" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Es gibt keinen User-Story Status mit dieser id" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "Die E-Mailadresse ist bereits vergeben" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Es gibt keinen Aufgabenstatus mit dieser id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Ungültige Rolle für dieses Projekt" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "Der Projekteigentümer muss Administrator sein." + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" +"Mindestens ein Benutzer muss ein aktiver Administrator des Projektes sein." + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Voreingestellte Optionen" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Status für User-Stories" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Punkte" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Aufgaben Status" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Ticket Status" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Ticket Arten" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioritäten" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Gewichtung" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Rollen" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Stimmen" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Stimme" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'content' Parameter ist erforderlich" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id' Parameter ist erforderlich" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "letzte Änderung" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Prüfe die API der Historie auf Übereinstimmung" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "Projektmitglied" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "Projektmitglieder" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "Id" @@ -3771,54 +3972,54 @@ msgstr "Einschränkungen" msgid "Important dates" msgstr "Wichtige Termine" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Doppelte E-Mail" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Ungültige E-Mail" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Ungültiger Benutzername oder E-Mail" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "E-Mail erfolgreich gesendet." -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Aktueller Passwort Parameter wird benötigt" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Neuer Passwort Parameter benötigt" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Ungültige Passwortlänge, mindestens 6 Zeichen erforderlich" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Ungültiges aktuelles Passwort" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Ungültig. Sind Sie sicher, dass das Token korrekt ist und Sie es nicht " "bereits verwendet haben?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Ungültig. Sind Sie sicher, dass das Token korrekt ist?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "Superuser Status" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3826,25 +4027,25 @@ msgstr "" "Dieser Benutzer soll alle Berechtigungen erhalten, ohne dass diese zuvor " "zugewiesen werden müssen. " -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "Benutzername" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Benötigt. 30 Zeichen oder weniger.. Buchstaben, Zahlen und /./-/_ Zeichen" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Geben Sie einen gültigen Benuzternamen ein." -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "aktiv" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3852,71 +4053,63 @@ msgstr "" "Kennzeichnet den Benutzer als aktiv. Deaktiviere die Option anstelle einen " "Benutzer zu löschen." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "Über mich" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "Foto" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "Beitrittsdatum" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "Vorgegebene Sprache" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "Standard-Theme" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "Vorgegebene Zeitzone" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "Tag-Farben" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "E-Mail Token" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "neue E-Mail Adresse" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "Berechtigungen" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "ungültig" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein." @@ -4118,49 +4311,53 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Sie wurden taigatisiert! " -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Es gibt keine Rolle mit dieser id" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "ungültig" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" "Doppelter Schlüsselwert verstößt einzigartige Vorgaben. Schlüssel '{}' " "existiert bereits." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "Schlüssel" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "Geheimer Schlüssel" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "Status Code" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "Anfrage Daten" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "Anfrage Header" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "Antwort Daten" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "Antwort Header" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "Dauer" diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index 627ffe46..276e9350 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: 2016-08-24 18:09+0200\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -181,10 +181,10 @@ msgstr "" #: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 #: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 -#: taiga/projects/epics/api.py:212 taiga/projects/epics/api.py:288 -#: taiga/projects/issues/api.py:235 taiga/projects/mixins/ordering.py:59 -#: taiga/projects/tasks/api.py:258 taiga/projects/tasks/api.py:281 -#: taiga/projects/userstories/api.py:332 taiga/projects/userstories/api.py:381 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -341,11 +341,11 @@ msgstr "" msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:460 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "" -#: taiga/base/filters.py:133 taiga/base/filters.py:240 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 #: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "" @@ -745,11 +745,11 @@ msgstr "" #: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 #: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:510 taiga/projects/models.py:543 -#: taiga/projects/models.py:579 taiga/projects/models.py:601 -#: taiga/projects/models.py:635 taiga/projects/models.py:655 -#: taiga/projects/models.py:675 taiga/projects/models.py:707 -#: taiga/projects/models.py:727 taiga/users/admin.py:54 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "" @@ -767,7 +767,7 @@ msgstr "" #: taiga/projects/epics/models.py:54 #: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:731 taiga/projects/tasks/models.py:61 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 #: taiga/projects/userstories/models.py:94 msgid "description" msgstr "" @@ -805,7 +805,7 @@ msgstr "" #: taiga/projects/custom_attributes/models.py:45 #: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:735 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 #: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 #: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 #: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 @@ -831,7 +831,7 @@ msgid "" msgstr "" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/projects/admin.py:103 taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "" @@ -861,9 +861,9 @@ msgstr "" msgid "The payload is not a valid json" msgstr "" -#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:151 -#: taiga/projects/issues/api.py:135 taiga/projects/tasks/api.py:197 -#: taiga/projects/userstories/api.py:265 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "" @@ -871,7 +871,7 @@ msgstr "" msgid "Bad signature" msgstr "" -#: taiga/hooks/event_hooks.py:65 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " @@ -880,7 +880,7 @@ msgid "" "\"{comment_message}\"" msgstr "" -#: taiga/hooks/event_hooks.py:70 +#: taiga/hooks/event_hooks.py:71 #, python-brace-format msgid "" "Comment From {platform}:\n" @@ -888,31 +888,31 @@ msgid "" "> {comment_message}" msgstr "" -#: taiga/hooks/event_hooks.py:83 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "" -#: taiga/hooks/event_hooks.py:102 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" "Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " "profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -#: taiga/hooks/event_hooks.py:106 +#: taiga/hooks/event_hooks.py:107 #, python-brace-format msgid "Issue created from {platform}." msgstr "" -#: taiga/hooks/event_hooks.py:119 +#: taiga/hooks/event_hooks.py:120 msgid "Invalid issue information" msgstr "" -#: taiga/hooks/event_hooks.py:148 taiga/hooks/event_hooks.py:170 +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 msgid "unknown user" msgstr "" -#: taiga/hooks/event_hooks.py:155 +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" "{user_text} changed the status from [{platform} commit]({commit_url} \"See " @@ -921,7 +921,7 @@ msgid "" " - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/event_hooks.py:160 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" "Changed status from {platform} commit.\n" @@ -929,7 +929,7 @@ msgid "" " - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/event_hooks.py:178 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" "This {type_name} has been mentioned by {user_text} in the [{platform} commit]" @@ -937,17 +937,17 @@ msgid "" "\"{commit_message}\"" msgstr "" -#: taiga/hooks/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" "This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/event_hooks.py:202 +#: taiga/hooks/event_hooks.py:206 msgid "The referenced element doesn't exist" msgstr "" -#: taiga/hooks/event_hooks.py:218 +#: taiga/hooks/event_hooks.py:222 msgid "The status doesn't exist" msgstr "" @@ -1115,27 +1115,27 @@ msgstr "" msgid "Admin roles" msgstr "" -#: taiga/projects/admin.py:97 +#: taiga/projects/admin.py:100 msgid "Privacity" msgstr "" -#: taiga/projects/admin.py:109 +#: taiga/projects/admin.py:112 msgid "Modules" msgstr "" -#: taiga/projects/admin.py:117 +#: taiga/projects/admin.py:120 msgid "Default values" msgstr "" -#: taiga/projects/admin.py:123 +#: taiga/projects/admin.py:126 msgid "Activity" msgstr "" -#: taiga/projects/admin.py:128 +#: taiga/projects/admin.py:131 msgid "Fans" msgstr "" -#: taiga/projects/admin.py:142 taiga/projects/attachments/models.py:39 +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 #: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 #: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 #: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 @@ -1144,24 +1144,29 @@ msgstr "" msgid "owner" msgstr "" -#: taiga/projects/admin.py:195 +#: taiga/projects/admin.py:200 #, python-brace-format msgid "{count} successfully made public." msgstr "" -#: taiga/projects/admin.py:196 +#: taiga/projects/admin.py:201 msgid "Make public" msgstr "" -#: taiga/projects/admin.py:210 +#: taiga/projects/admin.py:215 #, python-brace-format msgid "{count} successfully made private." msgstr "" -#: taiga/projects/admin.py:211 +#: taiga/projects/admin.py:216 msgid "Make private" msgstr "" +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + #: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "" @@ -1215,11 +1220,11 @@ msgstr "" #: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 #: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:498 -#: taiga/projects/models.py:520 taiga/projects/models.py:557 -#: taiga/projects/models.py:585 taiga/projects/models.py:611 -#: taiga/projects/models.py:641 taiga/projects/models.py:661 -#: taiga/projects/models.py:685 taiga/projects/models.py:713 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 #: taiga/projects/notifications/models.py:74 #: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 #: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 @@ -1239,7 +1244,7 @@ msgstr "" #: taiga/projects/custom_attributes/models.py:47 #: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 #: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:738 taiga/projects/tasks/models.py:50 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 #: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 #: taiga/userstorage/models.py:31 msgid "modified date" @@ -1260,10 +1265,10 @@ msgstr "" #: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:514 taiga/projects/models.py:547 -#: taiga/projects/models.py:581 taiga/projects/models.py:605 -#: taiga/projects/models.py:637 taiga/projects/models.py:657 -#: taiga/projects/models.py:679 taiga/projects/models.py:709 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 #: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "" @@ -1346,7 +1351,7 @@ msgstr "" msgid "Already exists one with the same name." msgstr "" -#: taiga/projects/epics/api.py:91 +#: taiga/projects/epics/api.py:92 msgid "You don't have permissions to set this status to this epic." msgstr "" @@ -1369,10 +1374,10 @@ msgstr "" msgid "subject" msgstr "" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:518 -#: taiga/projects/models.py:553 taiga/projects/models.py:609 -#: taiga/projects/models.py:639 taiga/projects/models.py:659 -#: taiga/projects/models.py:683 taiga/projects/models.py:711 +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 #: taiga/users/models.py:139 msgid "color" msgstr "" @@ -1539,23 +1544,23 @@ msgstr "" msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:153 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "" -#: taiga/projects/issues/api.py:157 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "" -#: taiga/projects/issues/api.py:161 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "" -#: taiga/projects/issues/api.py:165 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "" -#: taiga/projects/issues/api.py:169 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "" @@ -1590,9 +1595,9 @@ msgid "Likes" msgstr "" #: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:512 taiga/projects/models.py:545 -#: taiga/projects/models.py:603 taiga/projects/models.py:677 -#: taiga/projects/models.py:729 taiga/projects/wiki/models.py:35 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "" @@ -1605,9 +1610,9 @@ msgstr "" msgid "estimated finish date" msgstr "" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:516 -#: taiga/projects/models.py:549 taiga/projects/models.py:607 -#: taiga/projects/models.py:681 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "" @@ -1652,7 +1657,7 @@ msgstr "" msgid "invitation extra text" msgstr "" -#: taiga/projects/models.py:88 taiga/projects/models.py:733 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "" @@ -1708,31 +1713,31 @@ msgstr "" msgid "total story points" msgstr "" -#: taiga/projects/models.py:169 taiga/projects/models.py:744 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:746 +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "" -#: taiga/projects/models.py:173 taiga/projects/models.py:748 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "" -#: taiga/projects/models.py:175 taiga/projects/models.py:750 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "" -#: taiga/projects/models.py:177 taiga/projects/models.py:752 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "" -#: taiga/projects/models.py:180 taiga/projects/models.py:755 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "" -#: taiga/projects/models.py:182 taiga/projects/models.py:757 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "" @@ -1805,64 +1810,64 @@ msgstr "" msgid "activity last year" msgstr "" -#: taiga/projects/models.py:499 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "" -#: taiga/projects/models.py:551 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "" -#: taiga/projects/models.py:555 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:583 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "" -#: taiga/projects/models.py:741 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "" -#: taiga/projects/models.py:759 +#: taiga/projects/models.py:760 msgid "default options" msgstr "" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "" -#: taiga/projects/models.py:762 taiga/projects/userstories/models.py:43 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 #: taiga/projects/userstories/models.py:76 msgid "points" msgstr "" -#: taiga/projects/models.py:763 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "severities" msgstr "" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "roles" msgstr "" @@ -1900,7 +1905,7 @@ msgstr "" msgid "Notify exists for specified user and project" msgstr "" -#: taiga/projects/notifications/services.py:425 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "" @@ -2506,23 +2511,23 @@ msgid "You have reached your current limit of memberships for public projects" msgstr "" #: taiga/projects/services/projects.py:94 -#: taiga/projects/services/projects.py:134 taiga/users/services.py:581 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" #: taiga/projects/services/projects.py:98 -#: taiga/projects/services/projects.py:138 taiga/users/services.py:584 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" #: taiga/projects/services/projects.py:102 -#: taiga/projects/services/projects.py:142 taiga/users/services.py:588 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" #: taiga/projects/services/projects.py:106 -#: taiga/projects/services/projects.py:146 taiga/users/services.py:591 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" @@ -2588,15 +2593,15 @@ msgstr "" msgid "The tag doesn't exist." msgstr "" -#: taiga/projects/tasks/api.py:94 taiga/projects/tasks/api.py:103 +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:97 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:100 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -3212,34 +3217,34 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:116 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:120 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:210 +#: taiga/projects/userstories/api.py:213 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:217 +#: taiga/projects/userstories/api.py:220 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:232 +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:293 +#: taiga/projects/userstories/api.py:296 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:299 msgid "project or project_slug param is needed" msgstr "" diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index 736b8087..552d6ff8 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -17,9 +17,9 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-07-12 19:06+0000\n" -"Last-Translator: Jorge Sanchez \n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" +"Last-Translator: Taiga Dev Team \n" "Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/es/)\n" "MIME-Version: 1.0\n" @@ -28,162 +28,166 @@ msgstr "" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "El registro público está deshabilitado." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "Tipo de registro inválido" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "Tipo de login inválido" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Nombre de usuario no disponible" + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Email no disponible" + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "El token no pertenece a ninguna invitación válida." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Este usuario ya está registrado." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Este usuario ya es miembro del proyecto." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Error al crear un nuevo usuario " + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Token inválido" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "nombre de usuario no válido" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Son necesarios. 255 caracteres o menos (letras, números y /./-/_)" -#: 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:94 -msgid "Token not matches any valid invitation." -msgstr "El token no pertenece a ninguna invitación válida." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Este usuario ya está registrado." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "Este usuario ya es miembro del proyecto." - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Error al crear un nuevo usuario " - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Token inválido" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Este campo es requerido." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Valor inválido." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "El valor para '%s' debe ser True o False." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Escribe un slug válido que esté formado por letras, números o los símbolos " "de guión o subrayado." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Seleccione una opción válida. %(value)s no es una de las opciones " "disponibles." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Introduzca una dirección de email válida." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "" "La fecha posee un formato inválido. Utiliza alguno de los siguientes " "formatos: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "" "La fecha y hora poseen un formato inválido. Utiliza alguno de los siguientes " "formatos: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "" "El tiempo indicado posee un formato inválido. Utiliza alguno de los " "siguientes formatos: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Introduce un número entero" -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Asegúrate de que el valor es menor o igual a %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Asegúrate de que el valor es mayor o igual a %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "El valor \"%s\" debe ser un número en coma flotante." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Introduce un número." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Asegúrate de que no haya más de %s dígitos en total." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Asegúrate de que no haya más de %s decimales." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "" "Asegúrate de que no haya más de %s dígitos en la parte entera del número." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "No se ha adjuntado ningún archivo. Comprueba el encoding en el formulario." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "No se envió el archivo" -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "El archivo enviado está vacío." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -191,193 +195,190 @@ msgstr "" "Asegúrate de que el nombre del fichero contiene menos de %(max)d caracteres " "(ahora tiene %(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "Por favor, adjunta un fichero o marca la casilla de vacío, no ambos." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Elemento bloqueado" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "La página no es 'last' o no es un número." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Página no válida (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Definición de permiso inválida." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "PK '%s' inválida - el objeto no existe." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "" "Tipo incorrecto. Se esperaba un identificador (pk) y se ha recibido %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "El objeto con %s=%s no existe." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Hipervínculo inválido - La URL no encaja con ningun objeto." -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Hipervínculo inválido - La URL es incorrecta" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Hipervínculo inválido debido a un error de configuración" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Hipervínculo inválido - el objeto no existe." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Tipo incorrecto. Se esperaba una url y se ha recibido %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Datos invalidos" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "No se han introducido datos." -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "No se pueden crear nuevos objetos. Sólo está permitida la actualización de " "los existentes." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Se esperaba una lista de objetos." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "No encontrado" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Permiso denegado." -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Error en la aplicación del servidor." -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Error de conexión" -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Petición con formato incorrecto." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Credenciales de autenticación incorrectas." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "No se han proporcionado las credenciales de autenticación." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "No tienes permisos para realizar esta acción." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Método '%s' no permitido." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "No se ha podido satisfacer la perición de cabecera Accept" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Típo de medio '%s' no soportado." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Demasiadas peticiones." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Estará disponible en %d segundos%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Error inesperado" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "No encontrado." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Método no soportado por este recurso." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Argumentos erróneos." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Error de validación de datos" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Error de integridad por argumentos incorrectos o inválidos" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Error por incumplimiento de precondición" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "No hay espacio para mas proyectos" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Error en los típos de parámetros de filtrado" -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' debe ser un valor entero." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "etiquetas" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -432,7 +433,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -444,22 +445,6 @@ msgid "" " \n" " " msgstr "" -"\n" -"Soporte de Taiga:\n" -"%(support_url)s\n" -"
\n" -"Contáctanos:\n" -"\n" -"%(support_email)s\n" -"\n" -"
\n" -"Lista de correo:\n" -"\n" -"%(mailing_list_url)s\n" -"" #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -511,103 +496,88 @@ msgstr "" "\n" "Comentario: %(comment)s" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Necesitamos al menos un rol" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Se necesita el fichero con los datos exportados" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Formato de fichero de exportación inválido" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" no se ha encontrado en este proyecto" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Contiene attributos personalizados inválidos." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Nombre duplicado para el proyecto" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "error importando los datos del proyecto" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "error importando los roles" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "error importando los miembros" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "error importando la listados de valores de attributos del proyecto" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "error importando los valores por defecto de los atributos del proyecto" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "error importando los atributos personalizados" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "error importando los sprints" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "error importando las historias de usuario" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "error importando las tareas" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "error importando las peticiones" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "error importando las historias de usuario" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "error importando las tareas" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "error importando las páginas del wiki" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "error importando los enlaces del wiki" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "error importando las etiquetas" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "error importando los timelines" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "Error inesperado al importar el proyecto" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Erro generando el volcado de datos del proyecto" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -642,15 +612,15 @@ msgstr "" "TRACE ERROR:\n" "------------" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Error cargando el volcado de datos del proyecto" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "Error cargando el archivo del proyecto exportado" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "-- sin informacion --" @@ -889,77 +859,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Tu proyecto ha sido importado" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" no se ha encontrado en este proyecto" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Contenido inválido. Debe ser {\"clave\": \"valor\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Contiene attributos personalizados inválidos." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Nombre duplicado para el proyecto" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Se requiere autenticación" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nombre" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "URL del icono" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "descripción" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Siguiente URL" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "clave secreta para cifrar los tokens de aplicación" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "usuario" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "aplicación" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "nombre completo" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "dirección de email" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "comentario" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "fecha de creación" @@ -989,7 +979,7 @@ msgstr "" "

%(comment)s

" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Información extra" @@ -1023,388 +1013,345 @@ msgstr "" "\n" "[Taiga] Feedback de %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "El payload no es un json válido" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "El proyecto no existe" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Firma errónea" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "El elemento referenciado no existe" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "El estado no existe" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Estado cambiado desde un commit de BitBucket" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Información inválida de Issue" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"Petición creada por [@{bitbucket_user_name}]({bitbucket_user_url} \"Ver el " -"perfil de @{bitbucket_user_name} en BitBucket\") desde BitBucket.\n" -"Petición de origen en BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Ir a 'bb#{number} - {subject}'\"):\n" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" "\n" -"{description}" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Petición creada desde BitBucket." - -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Información de comentario de Issue inválida" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -"Comentario de [@{bitbucket_user_name}]({bitbucket_user_url} \"\"Ver el " -"perfil de @{bitbucket_user_name} en BitBucket\") desde BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Información inválida de Issue" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Comentario desde BitBucket:\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"[@{github_user_name}]({github_user_url} \"Ver el perfil de " -"@{github_user_name} en GitHub\") ha cambiado el estado a través del commit " -"de GitHub [{commit_id}]({commit_url} \"Ver commit '{commit_id} - " -"{commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Estado cambiado a través de un commit en GitHub." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Petición creada por [@{github_user_name}]({github_user_url} \"Ver el perfil " -"de @{github_user_name} en GitHub\") a través de la petición de GitHub " -"[gh#{number} - {subject}]({github_url} \"Ir a 'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Petición creada a través de GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"Commentario de [@{github_user_name}]({github_user_url} \"Ver el perfil de " -"GitHub de @{github_user_name}\") a través de la petición de Github " -"[gh#{number} - {subject}]({github_url} \"Ir a 'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Comentario a través de GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "El elemento referenciado no existe" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Estado cambiado desde un commit de GitLab" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "El estado no existe" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Creado desde Gitlab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Comentario de [@{gitlab_user_name}]({gitlab_user_url} \"Ver el perfil de " -"@{gitlab_user_name}'s en GitLab\") desde GitLab.\n" -"Petición de origen de GitLab: [gl#{number} - {subject}]({gitlab_url} \"Ir a " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Comentario desde GitLab:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Ver proyecto" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Ver sprints" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Ver historias de usuarios" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Ver tareas" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Ver peticiones" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Ver páginas del wiki" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Ver enlaces del wiki" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Solicitar afiliación" - -#: 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 "Agregar comentarios a historia de usuario" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Agregar comentarios a tareas" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Añadir peticiones" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Añadir comentarios a peticiones" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Agregar pagina wiki" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Modificar pagina wiki" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Agregar enlace wiki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modificar enlace wiki" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Añadir sprint" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modificar sprint" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Borrar sprint" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Ver historia de usuario" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Agregar historia de usuario" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modificar historia de usuario" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Borrar historia de usuario" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Agregar tarea" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modificar tarea" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Borrar tarea" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Añadir petición" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modificar petición" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Borrar petición" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Agregar pagina wiki" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Modificar pagina wiki" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Borrar pagina wiki" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Agregar enlace wiki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modificar enlace wiki" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Borrar enlace wiki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Modificar proyecto" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Agregar miembro" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Eliminar miembro" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Eliminar proyecto" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Agregar miembro" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Eliminar miembro" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Administrar valores de proyecto" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Administrar roles" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "Dueño" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Argumentos incompletos" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Formato de imagen no válido" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Nombre de plantilla invalido" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Descripción de plantilla invalida" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "id de usuario inválido" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "El usuario no existe" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "El usuario debe ser un miembro del proyecto" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" @@ -1412,158 +1359,233 @@ msgstr "" "El proyecto debe tener un dueño y al menos uno de los usuarios debe ser un " "administrador activo" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "No tienes suficientes permisos para ver esto." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "La actualización parcial no está soportada." -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "Proyecto" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "típo de contenido" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "id de objeto" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "fecha modificada" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "archivo adjunto" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "está desactualizado" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "orden" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Personalizado" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "El proyecto esta bloqueado por un fallo en el pago" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "El proyecto esta bloqueado por los administradores" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "El proyecto esta bloqueado porque el dueño ha salido" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Texto" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Texto multilínea" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Fecha" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "tipo" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "valores" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "historia de usuario" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "tarea" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "petición" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Ya existe uno con el mismo nombre." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "estado" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "asunto" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "color" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "asignado a" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "requerido por el cliente" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "requerido por el equipo" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "El comentario ya ha sido borrado." -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "El comentario no se borro." -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Cambio" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Crear" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Borrar" @@ -1619,7 +1641,7 @@ msgstr "borrado" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "No asignado" @@ -1666,95 +1688,75 @@ msgstr "De:" msgid "To:" msgstr "A:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "contenido" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "nota de bloqueo" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "No tienes permisos para asignar un sprint a esta petición." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "No tienes permisos para asignar un estado a esta petición." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "No tienes permisos para establecer la gravedad de esta petición." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "No tienes permiso para establecer la prioridad de esta petición." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "No tienes permiso para establecer el tipo de esta petición." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "estado" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "gravedad" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "prioridad" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "sprint" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "fecha de finalización" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "asunto" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "asignado a" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "referencia externa" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Like" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Likes" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1766,8 +1768,9 @@ msgstr "fecha estimada de comienzo" msgid "estimated finish date" msgstr "fecha estimada de finalización" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "está cerrada" @@ -1781,120 +1784,132 @@ msgstr "" "La fecha de inicio estimada debe ser previa a la fecha de finalización " "estimada." -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "No hay sprints con este id" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "está bloqueada" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "el parámetro '{param}' es obligatório" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "el parámetro 'project' es obligatório" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "email" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "creado el" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "texto extra de la invitación" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "orden del usuario" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "El usuario ya es miembro del proyecto" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "puntos por defecto" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "estado de historia por defecto" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "puntos por defecto" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "estado de tarea por defecto" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "prioridad por defecto" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "gravedad por defecto" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "estado de petición por defecto" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "tipo de petición por defecto" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "miembros" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "total de sprints" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "puntos de historia totales" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "panel de backlog activado" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "panel de kanban activado" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "panel de wiki activo" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "panel de peticiones activo" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "sistema de videoconferencia" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "datos extra de videoconferencia" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "creación de plantilla" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "privado" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "permisos de anónimo" @@ -1902,170 +1917,252 @@ msgstr "permisos de anónimo" msgid "user permissions" msgstr "permisos de usuario" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "privado" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "es destacado" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "está buscando a gente" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "nota (buscando a gente)" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "colores de etiquetas" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "token de transferencia de proyecto" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "código bloqueado" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "fecha y hora de actualización" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "recuento" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "fans la última semana" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "fans el último mes" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "fans el último año" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "actividad la última semana" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "actividad el último mes" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "actividad el último áño" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "configuración de modulos" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "archivado" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "color" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "limite del trabajo en progreso" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "valor" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "rol por defecto para el propietario" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "opciones por defecto" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "estatuas de historias" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "puntos" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "estatus de tareas" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "estados de petición" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "tipos de petición" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "gravedades" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "roles" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Involucrado" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Todas" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Ninguna" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "fecha y hora de creación" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "entradas del histórico" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "usuarios notificados" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Observado" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "" "Ya existe una política de notificación para este usuario en el proyecto." -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Valor inválido para el nivel de notificación" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2773,146 +2870,139 @@ msgstr "" "\n" "[%(project)s] Borrada la página del wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Los observadores tienen usuarios invalidos" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "La versión debe ser un número entero" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "La versión no es válida" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Las version difiere de la actual" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "versión" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" "No puedes abandonar el proyecto si eres el dueño o no existen más " "administradores" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "La dirección de email ya está en uso." - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Rol inválido para el proyecto" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." -msgstr "El dueño del proyecto debe ser administrador" - -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -"Por lo menos un usuario debe ser administrador activo para este proyecto" -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Opciones por defecto" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Estados de historia de usuario" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Puntos" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Estado de tareas" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Estados de peticion" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Tipos de petición" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Prioridades" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Gravedades" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Roles" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "Ha alcanzado el limite de miembros para proyectos privados" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "Ha alcanzado el limite de miembros para proyectos públicos" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "No puedes tener más proyectos privados" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" "Este proyecto alcanzo el limite actual de miembros para proyectos privados" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "No puedes tener más proyectos públicos" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" "Este proyecto alcanzo su limite actual de miembros para proyectos publicos" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Sprint futuro" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Final de proyecto" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "token inválido" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "El token ha expirado" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "etiquetas" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "colores de etiquetas" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "No tienes permisos para asignar este sprint a esta tarea." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "No tienes permisos para asignar esta historia a esta tarea." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "No tienes permisos para asignar este estado a esta tarea." @@ -2928,9 +3018,35 @@ msgstr "orden en el taskboard" msgid "is iocaine" msgstr "tiene iocaína" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "No existe ninguna tarea con este id" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3388,12 +3504,12 @@ msgstr "" "[%(project)s] Oferta de transferencia de dominio del proyecto\n" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3411,12 +3527,12 @@ msgstr "" "las nuevas necesidades del cliente." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3432,305 +3548,391 @@ msgstr "" "diferentes colas." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Nueva" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Preparada" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "En curso" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Lista para testear" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Hecha" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Archivada" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Cerrada" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Necesita información" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Pospuesta" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Rechazada" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Bug" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Pregunta" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Mejora" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Baja" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Alta" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Deseada" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Menor" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Importante" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Crítica" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Diseñador" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Product Owner" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "No tienes permisos para asignar este sprint a esta historia de usuario." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "No tienes permisos para asignar este estado a esta historia de usuario." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Generada la historia de usuario #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "orden en el backlog" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "orden en el sprint" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "fecha de finalización" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "requerido por el cliente" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "requerido por el equipo" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "generada desde una petición" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "No existe ninguna historia de usuario con este id" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "No existe ningún proyecto con este id" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "No existe ningún estado de historia con este id" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "La dirección de email ya está en uso." -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "No existe ningún estado de tarea con este id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Rol inválido para el proyecto" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "El dueño del proyecto debe ser administrador" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" +"Por lo menos un usuario debe ser administrador activo para este proyecto" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Opciones por defecto" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Estados de historia de usuario" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Puntos" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Estado de tareas" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Estados de peticion" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Tipos de petición" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioridades" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Gravedades" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Roles" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Votos" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Voto" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "el parámetro 'content' es obligatório" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "el parámetro 'project_id' es obligatório" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "última modificación por" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Comprueba la API de histórico para obtener el diff exacto" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "Miembro del proyecto" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "Miembros del proyecto" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "Id" @@ -3758,53 +3960,53 @@ msgstr "Restricciones" msgid "Important dates" msgstr "datos importántes" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Email duplicado" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Email no válido" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Nombre de usuario o email no válidos" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "¡Correo enviado con éxito!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "La contraseña actual es obligatoria." -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "La nueva contraseña es obligatoria" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "La longitud de la contraseña debe de ser de al menos 6 caracteres" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Contraseña actual inválida" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Invalido, ¿estás seguro de que el token es correcto y no se ha usado antes?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Inválido, ¿estás seguro de que el token es correcto?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "es superusuario" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3812,24 +4014,24 @@ msgstr "" "Otorga todos los permisos a este usuario sin necesidad de hacerlo " "explicitamente." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "nombre de usuario" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Obligatorio. 30 caracteres o menos. Letras, números y /./-/_" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Introduce un nombre de usuario válido" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "activo" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3837,71 +4039,63 @@ msgstr "" "Denota a los usuarios activos. Desmárcalo para dar de baja/borrar a un " "usuario." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografía" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "foto" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "fecha de registro" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "idioma por defecto" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "tema por defecto" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "zona horaria por defecto" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "añade color a las etiquetas" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "token de email" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "nueva dirección de email" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "numero maximo de proyectos privados asignados" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "numero maximo de proyectos publicos asignados" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "máximo de membresías para cada proyecto privado poseído" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "máximo de membresías para cada proyecto público poseído" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "permisos" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "no válido" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Nombre de usuario inválido. Prueba con otro." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Nombre de usuario o contraseña inválidos." @@ -4092,47 +4286,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "¡Te hemos Taigaizado!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "No existe ningún rol con este id" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "no válido" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Nombre de usuario inválido. Prueba con otro." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "Violación de una restricción de unicidad. La clave '{}' ya existe." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "clave" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "clave secreta" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "código de estado" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "datos de petición" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "cabeceras de la petición" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "datos de respuesta" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "cabeceras de la respuesta" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "duración" diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po index e36fb68a..f34af666 100644 --- a/taiga/locale/fi/LC_MESSAGES/django.po +++ b/taiga/locale/fi/LC_MESSAGES/django.po @@ -10,9 +10,9 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-08-14 23:29+0000\n" -"Last-Translator: Sami Singh \n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" +"Last-Translator: Taiga Dev Team \n" "Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fi/)\n" "MIME-Version: 1.0\n" @@ -21,164 +21,168 @@ msgstr "" "Language: fi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Julkinen rekisteri on suljettu." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "väärä rekisterin tyyppi" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "väärä kirjautumistyyppi" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Käyttäjänimi on varattu." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Sähköposti on jo varattu." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Tunniste ei vastaa mihinkään avoimeen kutsuun." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Käyttäjä on jo rekisteröitynyt." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Tämä käyttäjä on jo projektin jäsen." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Virhe käyttäjän luonnissa." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Väärä tunniste" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "tuntematon käyttäjänimi" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "Pakollinen. Korkeintaan 255 merkkiä. Kirjaimia, numeroita /./-/_ merkkejä'" -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Käyttäjänimi on varattu." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "Sähköposti on jo varattu." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Tunniste ei vastaa mihinkään avoimeen kutsuun." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Käyttäjä on jo rekisteröitynyt." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "Tämä käyttäjä on jo projektin jäsen." - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Virhe käyttäjän luonnissa." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Väärä tunniste" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Pakollinen kenttä." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Virheellinen arvo." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "'%s' pitää olla True tai False." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Anna kelvollinen 'avain' joka koostuu merkeistä, numeroista, alaviivoista ja " "tavuviivoista." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Valitse kelvollinen valinta. %(value)s ei ole yksi vaihtoehdoista." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Anna voimassaoleva sähköpostiosoite." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "Päivämäärä on väärässä muodossa. Käytä yhtä näistä muodoista: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "Päiväys on väärässä muodossa. Käytä yhtä näistä muodoista: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "Aika on väärässä muodossa. Käytä yhtä näistä muodoista: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Anna kokonaisluku." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Varmista että arvo on korkeintaan %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Varmista että arvo on vähintään %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" pitää olla desimaaliluku." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Anna numero." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Anna korkeintaan %s numeroa yhteensä." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Desimaaleja voi olla korkeintaan %s." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Ennen desimaalipistettä saa olla korkeintaan %s numeroa." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Tiedostoa ei lähtetty. Varmista merkistö lomakkeella." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Tiedostoa ei lähetetty." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Tiedosto oli tyhjä." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr "" "Tiedoston nimi saa olla korkeintaan %(max)d pitkä se on nyt %(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "Valitse tiedosto tai Poista valintaneliö, ei molempia." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -186,180 +190,177 @@ msgstr "" "Anna kelvollinen kuva. Annettu ei ollut tunnistettava kuva tai se oli " "vioittunut." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Estetty elementti" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Sivu ei ole 'viimeinen', ekä sitä pystytä muuntamaan numeroksi." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Virheellinen sivu (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Virheellinen oikeuksien määrittely." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Virheellinen pk '%s' - sitä ei löydy." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Väärä tyyppi. Odotetaan pk-arvoa, vastaanotettu %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Kohdetta jossa %s=%s ei ole." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Viallinen linkki - URL ei löydy" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Viallinen linkki - URL ei löydy" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Virheellinen linkki konfiguraatiovirheen takia" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Virheellinen linkki - kohdetta ei löydy." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Väärä tyyppi. Odotan URL-merkkijonoa, sain %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Virheellinen data" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Syöte puuttuu" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "En voi luoda uutta kohdetta, vain olemassaolevat voidaan päivittää." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Anna lista kohteista." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Ei löytynyt" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Ei käyttöoikeutta" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Palvelinsovelluksen virhe" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Yhteysvirhe." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Virheellinen pyyntö." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Virheelliset tunnistautumistiedot." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Kirjautumistiedot puuttuvat." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Sinulla ei ole tähän oikeuksia." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Method '%s' not allowed." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Could not satisfy the request's Accept header" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Unsupported media type '%s' in request." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Request was throttled." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Tulee saataville %d sekunttia %s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Odottamaton virhe" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Ei löytynyt." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Method not supported for this endpoint." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Väärät argumentit." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Data validation error" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Integrity Error for wrong or invalid arguments" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Precondition error" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "Ei enää tilaa uusille projekteille." -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Error in filter params types." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' must be an integer value." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "avainsanat" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -414,7 +415,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -426,26 +427,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" Taiga tuki:\n" -" %(support_url)s\n" -"
\n" -" Ota yhteyttä:\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Postituslista:\n" -" \n" -" %(mailing_list_url)s\n" -" \n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -497,103 +478,88 @@ msgstr "" "\n" "Kommentti: %(comment)s" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Tarvitsemme ainakin yhden roolin" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Tarvitaan tiedosto" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Virheellinen tiedostomuoto" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" ei löytynyt tästä projektista" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Virheellinen sisältä, pitää olla muodossa {\"avain\": \"arvo\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Sisältää vieheellisiä omia kenttiä." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Nimi on tuplana projektille" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "virhe projektidatan tuonnissa" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "virhe roolien tuonnissa" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "virhe jäsenyyksien tuonnissa" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "virhe atribuuttilistan tuonnissa" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "virhe oletusarvojen tuonnissa" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "virhe omien arvojen tuonnissa" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "virhe kierroksien tuonnissa" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "virhe käyttäjätarinoiden tuonnissa" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "virhe tehtävien tuonnissa" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "virhe pyyntöjen tuonnissa" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "virhe käyttäjätarinoiden tuonnissa" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "virhe tehtävien tuonnissa" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "virhe wiki-sivujen tuonnissa" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "virhe viki-linkkien tuonnissa" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "virhe avainsanojen sisäänlukemisessa" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "virhe aikajanojen tuonnissa" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "odottamaton virhe projektia tuotaessa" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Virhe tiedoston luonnissa" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -613,15 +579,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Virhe tiedoston latauksessa" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -857,77 +823,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Projetkisi tiedosto on luettu sisään" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" ei löytynyt tästä projektista" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Virheellinen sisältä, pitää olla muodossa {\"avain\": \"arvo\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Sisältää vieheellisiä omia kenttiä." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Nimi on tuplana projektille" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nimi" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "kuvaus" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "koko nimi" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "sähköpostiosoite" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "kommentti" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "luontipvm" @@ -959,7 +945,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Lisätiedot" @@ -993,522 +979,577 @@ msgstr "" "\n" "[Taiga] Palautetta käyttäjältä %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "The payload is not a valid json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Projektia ei löydy" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Virheellinen allekirjoitus" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "Viitattu elementtiä ei löydy" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Tilaa ei löydy" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Tila muutettu BitBucket kommitilla" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Virheellinen pyynnön tieto" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Virheellinen pyynnön kommentin tieto" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Virheellinen pyynnön tieto" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Tila muutettu GitHub commitin toimesta." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Pyyntö luotu [@{github_user_name}]({github_user_url} \"Katso " -"@{github_user_name}'s GitHub profile\") GitHubista.\n" -"ALkuperäinen GitHub pyyntö: [gh#{number} - {subject}]({github_url} \"Siirry " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Pyyntö luotu GitHubista" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Kommentti [@{github_user_name}]({github_user_url} \"Katso " -"@{github_user_name}'s GitHub profile\") GitHubista.\n" -"Alkuperäinen GitHub pyyntö: [gh#{number} - {subject}]({github_url} \"Siirry " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Kommentti GitHubista:\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Tila muutettu GitLab kommitilla" - -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Luotu GitLabissa" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Viitattu elementtiä ei löydy" -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Tilaa ei löydy" + +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Katso projektia" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Katso virstapylvästä" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Katso käyttäjätarinoita" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Katso tehtäviä" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Katso pyyntöjä" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Katso wiki-sivuja" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Katso wiki-linkkejä" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Pyydä jäsenyyttä" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Lisää käyttäjätarina projektiin" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Lisää kommentteja käyttäjätarinoihin" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Lisää kommentteja tehtäviin" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Lisää pyyntöjä" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Lisää kommentteja pyyntöihin" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Lisää wiki-sivu" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Muokkaa wiki-sivua" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Lisää wiki-linkki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Muokkaa wiki-linkkiä" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Lisää virstapylväs" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Muokkaa virstapyvästä" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Poista virstapylväs" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Katso käyttäjätarinaa" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Lisää käyttäjätarina" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Muokkaa käyttäjätarinaa" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Poista käyttäjätarina" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Lisää tehtävä" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Muokkaa tehtävää" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Poista tehtävä" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Lisää pyyntö" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Muokkaa pyyntöä" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Poista pyyntö" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Lisää wiki-sivu" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Muokkaa wiki-sivua" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Poista wiki-sivu" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Lisää wiki-linkki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Muokkaa wiki-linkkiä" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Poista wiki-linkki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Muokkaa projekti" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Lisää jäsen" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Poista jäsen" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Poista projekti" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Lisää jäsen" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Poista jäsen" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Hallinnoi projektin arvoja" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Hallinnoi rooleja" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "omistaja" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Puutteelliset argumentit" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Väärä kuvaformaatti" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Virheellinen mallipohjan nimi" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Virheellinen mallipohjan kuvaus" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Sinulla ei ole oikeuksia nähdä tätä." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "Projekti ID ei vastaa kohdetta ja projektia" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "projekti" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "sisältötyyppi" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "objekti ID" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "muokkauspvm" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "liite" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "on poistettu" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "order" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 -msgid "Text" +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:29 -msgid "Date" +msgid "Multi-Line Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:30 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "tyyppi" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "arvot" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "käyttäjätarina" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "tehtävä" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "pyyntö" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Nimi on jo olemassa" -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "viittaus" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "tila" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "aihe" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "väri" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "tekijä" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "on asiakkaan vaatimus" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "on tiimin vaatimus" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Kommentti on jo poistettu" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Kommenttia ei poistettu" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Muokkaa" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Luo" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Poista" @@ -1564,7 +1605,7 @@ msgstr "poistettu" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Tekijä puuttuu" @@ -1611,95 +1652,75 @@ msgstr "Keneltä:" msgid "To:" msgstr "Kenelle:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "sisältö" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "suljettu muistiinpano" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Sinulla ei ole oikeuksia laittaa kierrosta tälle pyynnölle." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Sinulla ei ole oikeutta asettaa statusta tälle pyyntö." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Sinulla ei ole oikeutta asettaa vakavuutta tälle pyynnölle." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "Sinulla ei ole oikeutta asettaa kiireellisyyttä tälle pyynnölle." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Sinulla ei ole oikeutta asettaa tyyppiä tälle pyyntö." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "viittaus" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "tila" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "vakavuus" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "kiireellisyys" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "virstapylväs" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "loppupvm" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "aihe" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "tekijä" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "ulkoinen viittaus" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "hukka-aika" @@ -1711,8 +1732,9 @@ msgstr "arvioitu alkupvm" msgid "estimated finish date" msgstr "arvioitu loppupvm" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "on suljettu" @@ -1724,120 +1746,132 @@ msgstr "disponibility" msgid "The estimated start must be previous to the estimated finish." msgstr "Alkuajan pitää olla ennen loppuaikaa." -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Kierrosta tällä ID:llä ei ole" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "on lukittu" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' parametri on pakollinen" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project' parametri on pakollinen" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "sähköposti" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "luo täällä" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "tunniste" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "kutsun lisäteksti" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "käyttäjäjärjestys" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "Käyttäjä on jo projektin jäsen" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "oletuspisteet" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "oletus Kt tila" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "oletuspisteet" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "oletus tehtävän tila" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "oletus kiireellisyys" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "oletus vakavuus" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "oletus pyynnön tila" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "oletus pyyntö tyyppi" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "jäsenet" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "virstapyväitä yhteensä" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "käyttäjätarinan yhteispisteet" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "aktiivinen odottavien paneeli" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "aktiivinen kanban-paneeli" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "aktiivinen wiki-paneeli" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "aktiivinen pyyntöpaneeli" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "videokokous järjestelmä" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "luo mallipohja" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "on yksityinen" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "vieraan oikeudet" @@ -1845,169 +1879,251 @@ msgstr "vieraan oikeudet" msgid "user permissions" msgstr "käyttäjän oikeudet" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "on yksityinen" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "avainsanojen värit" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "päivityspvm" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "moduulien asetukset" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "on arkistoitu" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "väri" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "työn alla olevien max" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "arvo" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "oletus omistajan rooli" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "oletus optiot" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "kt tilat" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "pisteet" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "tehtävän tilat" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "pyyntöjen tilat" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "pyyntötyypit" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "kiireellisyydet" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "vakavuudet" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "roolit" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "luontipvm" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "historian kohteet" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "ilmoita käyttäjille" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Ilmoita olemassaolosta määritellyille käyttäjille ja projektille" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2725,141 +2841,135 @@ msgstr "" "\n" "[%(project)s] Poistettiin wiki-sivu \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Vahdit sisältävät virheellisiä käyttäjiä" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "Versio pitää olla kokonaisluku" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Versio ei ole sama kuin nykyinen" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "versio" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Sähköpostiosoite on jo käytössä" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Virheellinen rooli projektille" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Oletusoptiot" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Käyttäjätarinatilat" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Pisteet" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Tehtävien tilat" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Pyyntöjen tilat" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "pyyntötyypit" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Kiireellisyydet" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Vakavuudet" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Roolit" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Tuleva kierros" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Projektin loppu" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Tunniste on virheellinen" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "avainsanat" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "avainsanojen värit" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2875,9 +2985,35 @@ msgstr "tehtävätaulun järjestys" msgid "is iocaine" msgstr "on hidaste" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "En löydä tehtävää tällä id:llä." +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3258,12 +3394,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3279,12 +3415,12 @@ msgstr "" "kasvaa ja muuttua kun tuotteesta ja asiakkaista opitaan lisää." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3297,303 +3433,388 @@ msgstr "" "jäsenille." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Uusi" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Valmis" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "Työn alla" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Valmis testattavaksi" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Tehty" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Arkistoitu" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Suljettu" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Tarvitsee lisätietoja" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Siirretty odottamaan" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Hylätty" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Virhe" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Kysymys" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Uusi ominaisuus" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Matala" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normaali" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Korkea" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Toivelista" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Vähäpätöinen" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Tärkeä" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Kriittinen" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "Käyttäjäkokemus" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Suunnittelu" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Edusta" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Palvelin" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Tuoteomistaja" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Sidosryhmä" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rooli" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "odottavien listan järjestys" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "kierros järjestys" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "loppupvm" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "on asiakkaan vaatimus" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "on tiimin vaatimus" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "luotu pyynnöstä" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "En löydä käyttäjätarinaa tällä id:llä" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "En löydä projektia tällä id:llä" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "En löydä käyttäjätarinan tilaa tällä id:llä" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "Sähköpostiosoite on jo käytössä" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "En löydä tehtävän tilaa tällä id:llä" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Virheellinen rooli projektille" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Oletusoptiot" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Käyttäjätarinatilat" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Pisteet" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Tehtävien tilat" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Pyyntöjen tilat" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "pyyntötyypit" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Kiireellisyydet" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Vakavuudet" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Roolit" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Ääniä" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Äänestä" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'content' parametri on pakollinen" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametri on pakollinen" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "viimeksi muokannut" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3621,151 +3842,143 @@ msgstr "" msgid "Important dates" msgstr "Tärkeät päivämäärät" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Sähköposti on jo olemassa" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Virheellinen sähköposti" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Tuntematon käyttäjänimi tai sähköposti" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Sähköposti lähetetty." -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Nykyinen salasanaparametri tarvitaan" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Uusi salasanaparametri tarvitaan" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Salasanan pitää olla vähintään 6 merkkiä pitkä" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Virheellinen nykyinen salasana" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Virheellinen. Oletko varma, että tunniste on oikea ja et ole jo käyttänyt " "sitä?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Virheellinen, oletko varma että tunniste on oikea?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "pääkäyttäjän status" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" "Kertoo että käyttäjä saa tehdä kaiken ilman erikseen annettuja oiekuksia." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "käyttäjänimi" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Vaaditaan. Korkeintaan 30merkkiä. Kirjaimet, numerot ja merkit /./-/_ " "sallittuja" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Anna olemassa oleva käyttäjänimi." -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "aktiivinen" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" "Käyttäjä on aktiivinen. Poista aktiivisuus käyttäjän poistamisen sijaan." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "kuva" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "liittymispvm" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "oletuskieli" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "oletus aikavyöhyke" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "väritä avainsanat" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "sähköpostitunniste" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "uusi sähköpostiosoite" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "oikeudet" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "virheellinen" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Tuntematon käyttäjänimi, yritä uudelleen." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Käyttäjätunnus tai salasana eivät ole oikein." @@ -3947,48 +4160,52 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Olet nyt Taigatettu!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "En löydä roolia tällä id:llä" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "virheellinen" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Tuntematon käyttäjänimi, yritä uudelleen." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" "Duplicate key value violates unique constraint. Key '{}' already exists." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "key" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "secret key" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "status code" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "request data" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "request headers" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "response data" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "response headers" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "duration" diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po index 61fa72db..1e218b2b 100644 --- a/taiga/locale/fr/LC_MESSAGES/django.po +++ b/taiga/locale/fr/LC_MESSAGES/django.po @@ -23,8 +23,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/fr/)\n" @@ -34,161 +34,165 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "L'inscription publique est désactivée." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "type d'inscription invalide" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "type d'identifiant invalide" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Ce nom d'utilisateur est déjà utilisé." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Cette adresse email est déjà utilisée." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Le jeton ne correspond à aucune invitation." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Cet utilisateur est déjà inscrit." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "L'utilisateur est déjà un membre du projet" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Erreur à la création du nouvel utilisateur." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Jeton invalide" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "nom d'utilisateur invalide" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "Requis. 255 caractères ou moins. Lettres, chiffres et caractères /./-/_'" -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Ce nom d'utilisateur est déjà utilisé." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "Cette adresse email est déjà utilisée." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Le jeton ne correspond à aucune invitation." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Cet utilisateur est déjà inscrit." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "L'utilisateur est déjà un membre du projet" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Erreur à la création du nouvel utilisateur." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Jeton invalide" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Ce champ est requis." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Valeur invalide." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "La valeur de '%s' doit être soit Vrai soit Faux." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Entrez un 'slug' valide composé de lettres, chiffres, tirets bas ou traits " "d'union." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Sélectionnez une option valide. %(value)s ne fait pas partie des choix " "possibles." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Entrez une adresse email valide." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "" "Le format de la date est mauvais. Utilisez un de ces formats à la place: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "" "Le format de l'horodatage est mauvais. Utilisez un de ces formats à la " "place: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "" "Le format de l'heure est mauvais. Utilisez un de ces formats à la place: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Entrez un nombre entier." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" "Assurez-vous que cette valeur est inférieure ou égale à %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" "Assurez-vous que cette valeur est supérieure ou égale à %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "La valeur de \"%s\" doit être un nombre en virgule flottante." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Entrez un nombre." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres au total." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Assurez-vous qu'il n'y a pas plus de %s décimales." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres avant le point." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Aucun fichier n'a été soumis. Vérifiez l'encodage sur le formulaire. " -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Aucun fichier n'a été soumis." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Le fichier soumis est vide." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -196,13 +200,13 @@ msgstr "" "Assurez-vous que le nom de fichier comporte au plus %(max)d caractères (il " "en a %(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Veuillez soit soumettre un fichier ou cocher la case de remise à zéro, mais " "pas les deux." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -210,184 +214,181 @@ msgstr "" "Envoyez une image valide. Le fichier que vous avez envoyé n'était pas une " "image ou était une image corrompue." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Élément bloqué" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" "La page n'est pas la \"dernière\", et ne peut pas non plus être convertie en " "entier." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Page invalide (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Définition de permission invalide." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Pk '%s' invalide - l'objet n'existe pas." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Type incorrect. Valeur pk attendue, %s reçu." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "L'objet pour lequel %s=%s n'existe pas." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Hyperlien invalide - aucune correspondance d'URL." -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Hyperlien invalide - Correspondance d'URL incorrecte." -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Hyperlien invalide dû à une erreur de configuration" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Hyperlien invalide - l'objet n'existe pas." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Type incorrect. Chaîne URL attendu, %s reçu." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Donnée invalide" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Aucune entrée fournie" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Impossible de créer un nouvel élément, seuls les éléments existants peuvent " "être mis à jour." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Une liste d'éléments était attendue." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Non trouvé" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Permission refusée" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Erreur du serveur d'application" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Erreur de connexion." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Requête mal formée." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Informations de connexion incorrects." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Informations d'authentification manquantes." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Vous n'avez pas l'autorisation d'effectuer cette action." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "La méthode %s n'est pas autorisée" -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Impossible de satisfaire l'en-tête Accept" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Type de média %s non pris en charge dans la requête." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "La requête a été limitée" -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Disponible dans %d seconde%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Erreur inattendue" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Non trouvé." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Méthode non supportée par ce point d'entrée" -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Arguments invalides." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Erreur de validation des données" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Erreur d'intégrité ou arguments invalides" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Erreur de précondition" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "Limite de projets atteinte." -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Erreur dans les types de paramètres de filtres" -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' doit être une valeur entière." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "tags" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -442,7 +443,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -454,26 +455,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" Support de " -"Taiga :\n" -" %(support_url)s\n" -"
\n" -" Nous contacter :" -"\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Groupe de " -"discussion :\n" -" \n" -" %(mailing_list_url)s\n" -" \n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -528,104 +509,89 @@ msgstr "" " Commentaire : %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Veuillez sélectionner au moins un rôle." -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Fichier de dump obligatoire" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Format de dump invalide" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" non trouvé dans the projet" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Format non valide. Il doit être de la forme {\"cle\": \"valeur\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Contient des champs personnalisés non valides." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Nom dupliqué pour ce projet" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "Erreur lors de l'importation de données" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "Erreur à l'importation des rôles" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "Erreur à l'importation des groupes d'utilisateurs" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "erreur lors de l'importation des listes des attributs de projet" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "" "erreur lors de l'importation des valeurs par défaut des attributs de projet" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "Erreur à l'importation des champs personnalisés" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "Erreur lors de l'importation des sprints." -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "erreur à l'importation des histoires utilisateur" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "Erreur lors de l'importation des tâches." - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "erreur à l'importation des problèmes" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "erreur à l'importation des histoires utilisateur" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "Erreur lors de l'importation des tâches." + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "Erreur à l'importation des pages Wiki" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "Erreur à l'importation des liens Wiki" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "erreur lors de l'importation des mots-clés" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "erreur lors de l'import des timelines" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Erreur dans la génération du dump du projet" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -645,15 +611,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Erreur au chargement du dump du projet" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -889,77 +855,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Votre projet à été importé" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" non trouvé dans the projet" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Format non valide. Il doit être de la forme {\"cle\": \"valeur\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Contient des champs personnalisés non valides." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Nom dupliqué pour ce projet" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Authentification requise" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nom" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "Url de l'icône" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "description" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Url suivante" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "Clé secrète pour chiffrer le jeton de l'application" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "utilisateur" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "application" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "Nom complet" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "Adresse email" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "Commentaire" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "Date de création" @@ -989,7 +975,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Informations supplémentaires" @@ -1023,356 +1009,345 @@ msgstr "" "\n" "[Taiga] Réaction de %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "Le payload n'est pas un json valide" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Le projet n'existe pas" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Signature non valide" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "L'élément référencé n'existe pas" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "L'état n'existe pas" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Statut changé depuis un commit BitBucket" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Information incorrecte sur le problème" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Ticket créé depuis BitBucket." +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Ignoré" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Information incorrecte sur le problème" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Commentaire depuis BitBucket :\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Statut changé depuis un commit GitHub." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Suivi de problème créé à partir de GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Commentaire provenant de GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "L'élément référencé n'existe pas" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Statut changé depuis un commit GitLab" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "L'état n'existe pas" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Créé à partir de GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Commentaire depuis GitLab:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Consulter le projet" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Voir les jalons" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Voir les histoires utilisateur" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Consulter les tâches" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Voir les problèmes" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Consulter les pages Wiki" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Consulter les liens Wiki" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Demander à devenir membre" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Ajouter l'histoire utilisateur au projet" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Ajouter des commentaires aux histoires utilisateur" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Ajouter des commentaires à une tâche" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Ajouter des problèmes" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Ajouter des commentaires aux problèmes" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Ajouter une page Wiki" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Modifier une page Wiki" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Ajouter un lien Wiki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modifier un lien Wiki" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Ajouter un jalon" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modifier le jalon" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Supprimer le jalon" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Voir l'histoire utilisateur" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Ajouter une histoire utilisateur" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modifier l'histoire utilisateur" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Supprimer l'histoire utilisateur" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Ajouter une tâche" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modifier une tâche" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Supprimer une tâche" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Ajouter un problème" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modifier le problème" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Supprimer le problème" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Ajouter une page Wiki" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Modifier une page Wiki" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Supprimer une page Wiki" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Ajouter un lien Wiki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modifier un lien Wiki" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Supprimer un lien Wiki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Modifier le projet" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Ajouter un membre" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Supprimer un membre" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Supprimer le projet" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Ajouter un membre" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Supprimer un membre" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Administrer les paramètres du projet" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Administrer les rôles" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "propriétaire" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "arguments manquants" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "format de l'image non valide" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Nom de modèle non valide" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Description du modèle non valide" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "Identifiant utilisateur invalide" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "L'utilisateur n'existe pas" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "L'utilisateur doit déjà être un membre du projet" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" @@ -1380,158 +1355,233 @@ msgstr "" "Le projet doit avoir un propriétaire et au moins l'un de ses membres doit " "être un administrateur actif." -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Vous n'avez pas les permissions pour consulter cet élément" -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Mises à jour partielles non supportées" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "projet" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "type du contenu" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "identifiant de l'objet" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "état modifié" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "pièces jointes" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "est obsolète" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "ordre" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Personnalisé" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "Ce projet a été bloqué pour cause d'impayé" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "Ce projet a été bloqué par l'équipe administrative" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "Ce projet est bloqué car son propriétaire est parti" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Texte" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Texte multi-ligne" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Date" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "type" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "valeurs" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "histoire utilisateur" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "tâche" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "problème" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Un élément de même nom existe déjà" -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "réf" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "état" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "sujet" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "couleur" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "assigné à" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "est un requis client" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "est un requis de l'équipe" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Commentaire déjà supprimé" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Commentaire non supprimé" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Changement" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Créer" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Supprimer" @@ -1587,7 +1637,7 @@ msgstr "supprimé" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Non assigné" @@ -1634,95 +1684,75 @@ msgstr "De :" msgid "To:" msgstr "A :" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "contenu" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "note bloquée" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Vous n'avez pas la permission d'affecter ce sprint à ce problème." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Vous n'avez pas la permission d'affecter cette sévérité à ce problème." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "Vous n'avez pas la permission d'affecter cette priorité à ce problème." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Vous n'avez pas la permission d'affecter ce type à ce problème." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "réf" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "état" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "sévérité" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "priorité" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "jalon" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "date de fin" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "sujet" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "assigné à" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "référence externe" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Aimer" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Aime" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1734,8 +1764,9 @@ msgstr "date de démarrage estimée" msgid "estimated finish date" msgstr "date de fin estimée" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "est fermé" @@ -1747,120 +1778,132 @@ msgstr "disponibilité" msgid "The estimated start must be previous to the estimated finish." msgstr "La date de démarrage doit être antérieure à la de fin prévisionnelle" -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Il n'y a pas de sprint avec cet id" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "est bloqué" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' paramètre obligatoire" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project' paramètre obligatoire" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "email" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "Créé le" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "jeton" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "Text supplémentaire de l'invitation" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "classement utilisateur" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "L'utilisateur est déjà un membre du projet" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "Points par défaut" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "statut de l'HU par défaut" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "Points par défaut" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "Etat par défaut des tâches" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "Priorité par défaut" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "Sévérité par défaut" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "statut du problème par défaut" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "type de problème par défaut" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "membres" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "total des jalons" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "total des points d'histoire" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "panneau backlog actif" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "panneau kanban actif" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "panneau wiki actif" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "panneau problèmes actif" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "plateforme de vidéoconférence" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "données complémentaires pour la salle de vidéoconférence" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "Modèle de création" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "est privé" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "Permissions anonymes" @@ -1868,169 +1911,251 @@ msgstr "Permissions anonymes" msgid "user permissions" msgstr "Permission de l'utilisateur" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "est privé" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "est mis en avant" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "est à la recherche de main d'oeuvre" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "couleurs des tags" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "jeton de transfert de projet" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "code bloqué" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "date de mise à jour" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "total" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "fans la semaine dernière" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "fans le mois dernier" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "fans l'année dernière" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "activité de la semaine écoulée" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "activité du mois écoulé" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "activité de l'année écoulée" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "Configurations des modules" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "est archivé" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "couleur" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "limite de travail en cours" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "valeur" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "rôle par défaut du propriétaire" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "options par défaut" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "statuts des us" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "points" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "états des tâches" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "statuts des problèmes" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "types de problèmes" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "priorités" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "sévérités" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "rôles" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Impliqué" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Toutes" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Aucun" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "date de création" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "entrées dans l'historique" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "notifier les utilisateurs" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Suivre" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "La notification existe pour l'utilisateur et le projet spécifiés" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Valeur non valide pour le niveau de notification" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2532,144 +2657,137 @@ msgstr "" "\n" "[%(project)s] Page Wiki \"%(page)s\" supprimée\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "La liste des observateurs contient des utilisateurs invalides" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "La version doit être un nombre entier" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "La version n'est pas valide" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "La version ne correspond pas à la version courante" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "version" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" "Vous ne pouvez pas quitter le projet si vous en êtes le propriétaire ou " "qu'il n'y a pas d'autre administrateur." -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Adresse email déjà existante" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Rôle non valide pour le projet" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." -msgstr "Le propriétaire du projet doit être un administrateur." - -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -"Au moins un utilisateur doit être un administrateur actif de ce projet." -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Options par défaut" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Etats de la User Story" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Points" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Etats des tâches" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Statuts des problèmes" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Types de problèmes" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Priorités" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Sévérités" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Rôles" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets privés" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "Vous avez atteint le nombre maximum d'adhésions à des projets publics" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "Vous avez atteint le nombre maximum de projets privés" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "Ce projet privé est le dernier que vous pouvez rejoindre" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "Vous avez atteint le nombre maximum de projets publics." -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "Ce projet public est le dernier que vous pouvez rejoindre" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Sprint futurs" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Fin du projet" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Jeton invalide" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "Le jeton est périmé" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "tags" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "couleurs des tags" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "Vous n'avez pas la permission d'affecter ce sprint à cette tâche." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "Vous n'avez pas la permission d'affecter ce récit à cette tâche." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème." @@ -2685,9 +2803,35 @@ msgstr "order du tableau de tâches" msgid "is iocaine" msgstr "est de l'iocaine" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Il n'existe pas de tâche avec cet identifant" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3075,12 +3219,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3098,12 +3242,12 @@ msgstr "" "sur le produit et sur ses utilisateurs." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3117,305 +3261,391 @@ msgstr "" "qui peuvent le consulter et y puiser leur travail." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Nouveau" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Prêt" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "En cours" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Prêt à tester" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Fait" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Archivé" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Fermé" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Infos manquantes" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Repoussé" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Rejeté" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Bug" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Question" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Amélioration" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Faible" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Fort" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Souhaits" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Mineur" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Important" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Critique" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "Expérience utilisateur" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Product Owner" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Participant" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Vous n'avez pas la permission d'affecter ce sprint à ce récit utilisateur." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "Vous n'avez pas la permission d'affecter ce statut à ce récit utilisateur." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rôle" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "order du backlog" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "ordre du sprint" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "date de fin" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "est un requis client" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "est un requis de l'équipe" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "généré depuis un problème" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Il n'y a pas d'user story avec cet id" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Aucun projet avec cet identifiant" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Il n'y a pas de statut d'user story avec cet id" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "Adresse email déjà existante" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Il n'y a pas de statut de tâche avec cet id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Rôle non valide pour le projet" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "Le propriétaire du projet doit être un administrateur." + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" +"Au moins un utilisateur doit être un administrateur actif de ce projet." + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Options par défaut" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Etats de la User Story" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Points" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Etats des tâches" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Statuts des problèmes" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Types de problèmes" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Priorités" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Sévérités" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Rôles" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Votes" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "vote" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'content' paramètre obligatoire" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id' paramètre obligatoire" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "dernier modificateur" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "id" @@ -3443,54 +3673,54 @@ msgstr "Restrictions" msgid "Important dates" msgstr "Dates importantes" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Email dupliquée" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Email non valide" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Nom d'utilisateur ou email non valide" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Mail envoyé avec succès!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Paramètre 'mot de passe actuel' requis" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Paramètre 'nouveau mot de passe' requis" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Le mot de passe doit être d'au moins 6 caractères" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Mot de passe actuel incorrect" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Invalide, êtes-vous sûre que le jeton est correct et qu'il n'a pas déjà été " "utilisé ?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Invalide, êtes-vous sûre que le jeton est correct ?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "statut superutilisateur" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3498,25 +3728,25 @@ msgstr "" "Indique que l'utilisateur a toutes les permissions sans avoir à lui les " "donner explicitement" -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "nom d'utilisateur" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Obligatoire. 30 caractères maximum. Lettres, nombres et les caractères /./-/_" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Entrez un nom d'utilisateur valide" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "actif" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3524,71 +3754,63 @@ msgstr "" "Indique qu'un utilisateur est considéré ou non comme actif. Désélectionnez " "cette option au lieu de supprimer le compte utilisateur." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biographie" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "photo" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "date d'inscription" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "langage par défaut" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "thème par défaut" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "Fuseau horaire par défaut" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "changer la couleur des tags" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "jeton email" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "nouvelle adresse email" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "permissions" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "invalide" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Aucun utilisateur avec ce nom ou ce mot de passe." @@ -3772,47 +3994,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Vous avez été Taigarisés!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Aucun rôle avec cet identifiant" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "invalide" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "Violation de clé primaire. La clé '{}' existe déjà." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "clé" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "clé secrète" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "code retour" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "données de la requête" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "en-têtes de la requête" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "données de la réponse" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "en-têtes de la réponse" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "durée" diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po index 8090f674..16ff2587 100644 --- a/taiga/locale/it/LC_MESSAGES/django.po +++ b/taiga/locale/it/LC_MESSAGES/django.po @@ -15,8 +15,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/it/)\n" @@ -26,156 +26,160 @@ msgstr "" "Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "La registrazione pubblica è disabilitata." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "Tipo di registrazione non valida" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "Tipo di login non valido" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Il nome utente scelto è già utilizzato." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "L'email inserita è già utilizzata." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Il token non corrisponde a nessun invito valido." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "L'Utente è già registrato." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Questo utente fa già parte del progetto." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Errore nella creazione della nuova utenza." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Token non valido" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "Nome utente non valido" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "Obbligatorio. Al massimo 255 caratteri, Contenenti: lettere, numeri e " "caratteri /./-/_ " -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Il nome utente scelto è già utilizzato." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "L'email inserita è già utilizzata." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Il token non corrisponde a nessun invito valido." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "L'Utente è già registrato." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "Questo utente fa già parte del progetto." - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Errore nella creazione della nuova utenza." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Token non valido" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Questo campo è obbligatorio." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Valore non valido." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "il valore di '%s' deve essere o Vero o Falso." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Uno 'slug' valido è composto da lettere, numeri, caratteri di sottolineatura " "o trattini." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Seleziona una scelta valida. %(value)s non è fra le scelte disponibili." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Inserisci un indirizzo e-mail valido." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "La data non ha un formato valido. Usa uno dei formati disponibili: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "L'orario non ha un formato valido. Usa uno dei formati disponibili: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "Formato temporale errato. Usa uno dei seguenti formati: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Inserisci il numero completo." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Assicurati che questo valore sia minore o uguale di %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Assicurati che questo valore sia maggiore o uguale di %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "il valore \"%s\" deve essere un valore \"float\"." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Inserisci un numero." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Assicurati che non ci siano più di %s cifre in totale." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Assicurati che non ci siano più di %s decimali." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Assicurati che non ci siano più di %s cifre prima del punto decimale." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "Non è stato caricato alcun file. Controlla il tipo di codifica nella scheda." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Nessun file caricato." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Il file caricato è vuoto." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -183,12 +187,12 @@ msgstr "" "Assicurati che il nome del file abbia al massimo %(max)d caratteri (ne ha " "%(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Carica il file oppure controlla la casella deselezionata. Non entrambi. " -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -196,183 +200,180 @@ msgstr "" "Carica un'immagina valida. Il file caricato potrebbe non essere un'immagine " "o l'immagine potrebbe essere corrotta. " -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "La pagina non è 'last', né può essere convertita come int." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Pagina (%(page_number)s) invalida: %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Definizione di permesso non valida." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "pk '%s' invalido - l'oggetto non esiste" -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Inserimento scorretto. Atteso un valore pk, ricevuto %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "L'oggetto con %s=%s non esiste." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Hyperlink invalido - nessun URL abbinato" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Hyperlink invalido - l'URL abbinato non è corretto" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "URL invalido a causa di un errore di configurazione" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Hyperlink invalido - l'oggetto non esiste" -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Inserimento scorretto. Attesa una stringa con URL, ricevuto %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Dati non validi" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Non è stato fornito nessun input" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Non è possibile creare un nuovo elemento, solo quelli esistenti possono " "essere aggiornati" -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Ci si aspetta una lista di oggetti." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Non trovato" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Permesso negato" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Errore sul server" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Errore di connessione" -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Richiesta composta erroneamente." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Le credenziali non sono corrette." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Le credenziali per l'autenticazione non sono state fornite." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Non hai il permesso per eseguire l'azione. " -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Metodo '%s' non permesso." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "" "Non è possibile soddisfare la richiesta di accettazione dell'intestazione." -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Nella richiesta è presente un contenuto media '%s' non supportato." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "La richiesta è stata soppressa" -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Disponibile in %d secondi%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Errore inaspettato" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Non trovato." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Metodo non supportato dall'endpoint." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Argomento errato." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Errore di validazione dei dati" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Errore di integrità causato da un argomento invalido o sbagliato" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Errore di precondizione" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Errore nel filtro del tipo di parametri." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'Progetto' deve essere un valore intero." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "tags" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -427,7 +428,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -439,33 +440,6 @@ msgid "" " \n" " " msgstr "" -"\n" -"Supporto Taiga:\n" -"\n" -"" -"%(support_url)s\n" -"\n" -"
\n" -"\n" -"Contact us:\n" -"\n" -"\n" -"\n" -"%(support_email)s\n" -"\n" -"\n" -"\n" -"
\n" -"\n" -"Mailing list:\n" -"\n" -"\n" -"\n" -"%(mailing_list_url)s\n" -"\n" -"" #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -519,104 +493,89 @@ msgstr "" "\n" "Commento: %(comment)s" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "C'è bisogno di almeno un ruolo" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "E' richiesto un file di dump" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Formato di dump invalido" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" non è stato trovato in questo progetto" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Contenuto errato. Deve essere {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Contiene campi personalizzati invalidi." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Il nome del progetto è duplicato" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "Errore nell'importazione del progetto dati" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "Errore nell'importazione i ruoli" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "Errore nell'importazione delle iscrizioni" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "Errore nell'importazione della lista degli attributi di progetto" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "" "Errore nell'importazione dei valori predefiniti degli attributi del progetto." -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "Errore nell'importazione degli attributi personalizzati" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "errore nell'importazione degli sprints" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "Errore nell'importazione delle user story" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "Errore nell'importazione dei compiti " - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "errore nell'importazione dei problemi" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "Errore nell'importazione delle user story" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "Errore nell'importazione dei compiti " + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "Errore nell'importazione delle pagine wiki" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "Errore nell'importazione dei link di wiki" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "Errore nell'importazione dei tags" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "Errore nell'importazione delle timelines" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Errore nella creazione del dump di progetto" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -636,15 +595,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Errore nel caricamento del dump di progetto" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -942,77 +901,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Il dump del tuo progetto è stato importato" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" non è stato trovato in questo progetto" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Contenuto errato. Deve essere {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Contiene campi personalizzati invalidi." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Il nome del progetto è duplicato" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "E' richiesta l'autenticazione" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nome" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "Url dell'icona" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "descrizione" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Url successivo" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "chiave segreta per cifrare i token dell'applicazione" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "utente" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "applicazione" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "Nome completo" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "Inserisci un indirizzo e-mail valido." -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "Commento" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "data creata" @@ -1042,7 +1021,7 @@ msgstr "" "

%(comment)s

" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Informazioni aggiuntive" @@ -1082,565 +1061,577 @@ msgstr "" "\n" "[Taiga] Hai un feedback da %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "Il carico non è un json valido" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Il progetto non esiste" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Firma non valida" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "L'elemento di riferimento non esiste" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Lo stato non esiste" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Lo stato è stato modificato a seguito di un commit di BitBucket" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Informazione sul problema non valida" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"Problema creato da [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") da BitBucket.\n" -"\n" -"Origine del problema su BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Go to 'bb#{number} - {subject}'\"):\n" -"\n" -"\n" -"\n" -"{description}" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Problema creato da BItBucket" +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Commento sul problema non valido" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -"Commento da [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") da BitBucket.\n" -"\n" -"Origine del problema da BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Go to 'bb#{number} - {subject}'\")\n" -"\n" -"\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Informazione sul problema non valida" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Commento da BitBucket:\n" -"\n" -"\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Stato cambiato da [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Lo stato è stato modificato da un commit su GitHub." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Problema creato da [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") su GitHub.\n" -"\n" -"Origine del problema su GitHub: [gh#{number} - {subject}]({github_url} \"Go " -"to 'gh#{number} - {subject}'\"):\n" -"\n" -"\n" -"\n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Problema creato su GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"Commento da [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") su GitHub.\n" -"Origine del problema su GitHub: [gh#{number} - {subject}]({github_url} \"Go " -"to 'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Commento su GitHub:\n" -"\n" -"\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "L'elemento di riferimento non esiste" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Lo stato è stato modificato tramite commit su GitLab" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Lo stato non esiste" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Creato da GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Commento da [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") su GitLab.\n" -"\n" -"Origine del problema su GitLab: [gl#{number} - {subject}]({gitlab_url} \"Go " -"to 'gl#{number} - {subject}'\")\n" -"\n" -"\n" -"\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Commento da GitLab:\n" -"\n" -"\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Vedi progetto" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Guarda le milestones" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Guarda le storie utente" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Guarda i compiti" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Guarda i problemi" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Guarda le pagine wiki" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Guarda i lik di wiki" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Richiedi l'iscrizione" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Aggiungi una storia utente al progetto" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Aggiungi dei commenti alle storia utente" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Aggiungi dei commenti ai compiti" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Aggiungi i problemi" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Aggiungi dei commenti ai problemi" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Aggiungi una pagina wiki" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Modifica la pagina wiki" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Aggiungi un link wiki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modifica il link di wiki" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Aggiungi una tappa" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modifica la tappa" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Elimina la tappa" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Guarda la storia utente" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Aggiungi una storia utente" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modifica una storia utente" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Cancella una storia utente" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Aggiungi un compito" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modifica il compito" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Elimina compito" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Aggiungi un problema" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modifica il problema" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Elimina il problema" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Aggiungi una pagina wiki" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Modifica la pagina wiki" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Elimina la pagina wiki" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Aggiungi un link wiki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modifica il link di wiki" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Elimina la pagina wiki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Modifica il progetto" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Aggiungi un membro" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Rimuovi il membro" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Elimina il progetto" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Aggiungi un membro" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Rimuovi il membro" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Valori dell'amministratore del progetto" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Ruoli dell'amministratore" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "proprietario" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Argomento non valido" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Formato dell'immagine non valido" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Il nome del template non è valido" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "La descrizione del template non è valida" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Non hai il permesso di vedere questo elemento." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Aggiornamento non parziale non supportato" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "L'ID di progetto non corrisponde tra oggetto e progetto" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "progetto" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "tipo di contenuto" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "ID dell'oggetto" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "data modificata" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "file allegato" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "non approvato" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "ordine" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "ApparIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Personalizzato" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Testo" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Testo multi-linea" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Data" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "tipo" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "valori" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "storia utente" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "compito" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "problema" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Ne esiste già un altro con lo stesso nome" -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "referenza" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "stato" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "soggeto" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "colore" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "assegnato a" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "é un requisito del cliente " + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "é una richiesta del team" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Il commento è già stato eliminato" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Commento non eliminato" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Cambiato" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Creato" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Eliminato" @@ -1696,7 +1687,7 @@ msgstr "rimosso" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Non assegnato" @@ -1743,95 +1734,75 @@ msgstr "Da:" msgid "To:" msgstr "A:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "contenuto" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "nota bloccata" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Non hai i permessi per aggiungere questo sprint a questo problema" -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Non hai i permessi per aggiungere questo stato a questo problema" -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Non hai i permessi per aggiungere questa criticità a questo problema" -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "Non hai i permessi per aggiungere questa priorità a questo problema." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Non hai i permessi per aggiungere questa tipologia a questo problema" -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "referenza" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "stato" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "criticità" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "priorità" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "tappa" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "data di conclusione" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "soggeto" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "assegnato a" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "referenza esterna" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Like" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Piaciuto" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "lumaca" @@ -1843,8 +1814,9 @@ msgstr "data stimata di inizio" msgid "estimated finish date" msgstr "data stimata di fine" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "è concluso" @@ -1857,120 +1829,132 @@ msgid "The estimated start must be previous to the estimated finish." msgstr "" "La data stimata di inizio deve essere precedente alla data stimata di fine." -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Non c'è nessuno sprint on questo ID" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "è bloccato" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "il parametro '{param}' è obbligatorio" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "il parametro 'project' è obbligatorio" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "email" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "creato a " -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "testo ulteriore per l'invito" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "ordine dell'utente" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "L'utente è già membro del progetto" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "punti predefiniti" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "stati predefiniti per le storie utente" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "punti predefiniti" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "stati predefiniti del compito" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "priorità predefinita" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "criticità predefinita" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "stato predefinito del problema" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "tipologia predefinita del problema" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "membri" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "tappe totali" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "punti totali della storia" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "pannello di backlog attivo" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "pannello kanban attivo" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "pannello wiki attivo" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "pannello dei problemi attivo" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "sistema di videoconferenza" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "ulteriori dati di videoconferenza " -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "creazione del template" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "è privato" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "permessi anonimi" @@ -1978,169 +1962,251 @@ msgstr "permessi anonimi" msgid "user permissions" msgstr "permessi dell'utente" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "è privato" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "in vetrina" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "sta cercando persone" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "note sulla ricerca delle persone " -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "colori dei tag" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "tempo e data aggiornati" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "conta" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "fans nella settimana" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "fans nel mese" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "fans nell'anno" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "attività nella settimana" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "attività nel mese" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "attività nell'anno" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "configurazione dei moduli" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "è archivitato" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "colore" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "limite dei lavori in corso" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "valore" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "ruolo proprietario predefinito" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "opzioni predefinite " -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "stati della storia utente" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "punti" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "stati del compito" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "stati del probema" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "tipologie del problema" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "priorità" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "criticità " -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "ruoli" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Coinvolto" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Tutti" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Nessuno" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "tempo e data creati" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "inserimenti della storia" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "notifica utenti" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Osservato" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "La notifica esiste per l'utente e il progetto specificati" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Valore non valido per il livello di notifica" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2998,142 +3064,136 @@ msgstr "" "\n" "[%(project)s] ha eliminato la pagina wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "L'osservatore contiene un utente non valido" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "La versione deve essere un intero" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "Il parametro della versione non è valido" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "La versione non corrisponde a quella corrente" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "versione" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "L'indirizzo email è già usato" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Ruolo di progetto non valido" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Opzioni predefinite" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Stati della storia utente" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Punti" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Stati del compito" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Stati del problema" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Tipologie del problema" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Priorità" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Criticità" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Ruoli" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Sprint futuri" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Termine di progetto" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Token non valido" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "tags" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "colori dei tag" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "Non hai i permessi per aggiungere questo sprint a questo compito." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" "Non hai i permessi per aggiungere questa storia utente a questo compito." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "Non hai i permessi per aggiungere questo stato a questo compito." @@ -3149,9 +3209,35 @@ msgstr "ordine del pannello dei compiti" msgid "is iocaine" msgstr "è sotto aspirina" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Non c'è nessun compito con questo ID" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3562,12 +3648,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3584,12 +3670,12 @@ msgstr "" "caratteristiche del prodotto e dei suoi clienti." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3603,304 +3689,389 @@ msgstr "" "membri del team, in modo che possano organizzare il lavoro." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Nuovo" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Pronto" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "In via di sviluppo" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Pronto per il test" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Fatto" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Archiviato" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Concluso" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Necessita di informazioni" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Postposto " #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Rifiutato" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Bug" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Domanda" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Miglioramento" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Basso" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normale" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Alto" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Lista dei desideri" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Minore" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Importante" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Critico" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Product Owner" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Non hai i permessi per aggiungere questo sprint a questa storia utente." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "Non hai i permessi per aggiungere questo stato a questa storia utente." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Stiamo generando la storia utente #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "ruolo" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "ordine del backlog" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "ordine dello sprint" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "data di termine" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "é un requisito del cliente " - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "é una richiesta del team" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "generato da un problema" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Non c'è nessuna storia utente con questo ID" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Non c'è nessuno progetto con questo ID" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Non c'è nessuno stato della storia utente con questo ID" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "L'indirizzo email è già usato" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Non c'è nessuno stato del compito con questo ID" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Ruolo di progetto non valido" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Opzioni predefinite" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Stati della storia utente" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Punti" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Stati del compito" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Stati del problema" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Tipologie del problema" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Priorità" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Criticità" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Ruoli" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Voti" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Voto" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "il parametro 'contenuto' è obbligatorio" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "Il parametro 'ID progetto' è obbligatorio" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "ultima modificatore" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Controlla le API della storie per la differenza esatta" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3928,54 +4099,54 @@ msgstr "" msgid "Important dates" msgstr "Date importanti" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "E-mail duplicata" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "E-mail non valida" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Username o e-mail non validi" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Mail inviata con successo!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "E' necessario il parametro della password corrente" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "E' necessario il parametro della nuovo password" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Lunghezza della password non valida, sono necessari almeno 6 caratteri" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Password corrente non valida" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Non valido. Sei sicuro che il token sia corretto e che tu non l'abbia già " "usato in precedenza?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Non valido. Sicuro che il token sia corretto?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "Stato del super-utente" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3983,26 +4154,26 @@ msgstr "" "Definisce che questo utente ha tutti i permessi senza assegnarglieli " "esplicitamente." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "nome utente" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Richiede 30 caratteri o meno. Deve comprendere: lettere, numeri e caratteri " "come /./-/_" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Inserisci un nome utente valido." -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "attivo" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -4010,71 +4181,63 @@ msgstr "" "Definisce se questo utente debba essere trattato come attivo. Deseleziona " "questo invece di eliminare gli account." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "fotografia" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "data di inizio partecipazione" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "lingua predefinita" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "tema predefinito" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "timezone predefinita" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "colora i tag" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "token e-mail" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "nuovo indirizzo e-mail" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "permessi" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "non valido" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Nome utente non valido. Provane uno diverso." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Il nome utente o la password non corrispondono all'utente." @@ -4302,49 +4465,53 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Sei stato Taigazzato!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Non c'è nessuno ruolo con questo ID" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "non valido" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Nome utente non valido. Provane uno diverso." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" "Un valore di chiave duplicato viola il vincolo unico. La chiave '{}' esiste " "già." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "chiave" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "chiave segreta" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "codice di stato" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "dati della richiesta" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "header della richiesta" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "dati della risposta" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "header della risposta" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "durata" diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po index e0d6070d..3962f37c 100644 --- a/taiga/locale/nl/LC_MESSAGES/django.po +++ b/taiga/locale/nl/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/nl/)\n" @@ -20,159 +20,163 @@ msgstr "" "Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Publieke registratie is uitgeschakeld." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "ongeldig registratie type" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "ongeldig login type" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Gebruikersnaame is al in gebruik." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "E-mail adres is al in gebruik." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Token stemt niet overeen met een geldige uitnodiging." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Gebruiker is al geregistreerd." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Fout bij het aanmaken van een nieuwe gebruiker." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Ongeldig token" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "ongeldige gebruikersnaam" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Verplicht. 255 tekens of minder. Letters, nummers en /./-/_ tekens'" -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Gebruikersnaame is al in gebruik." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "E-mail adres is al in gebruik." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Token stemt niet overeen met een geldige uitnodiging." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Gebruiker is al geregistreerd." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Fout bij het aanmaken van een nieuwe gebruiker." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Ongeldig token" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Dit veld is verplicht." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Ongeldige waarde." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "'%s' waarde moet True of False zijn." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Geef een geldige 'slug' in bestaande uit letters, nummers, underscores of " "koppeltekens." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Selecteer een geldige keuze. %(value)s is niet één van de aanwezige " "keuzemogelijkheden." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Voeg een geldig e-mail adres toe." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "" "Datum heeft het verkeerde formaat. Gebruik één van de volgende formaten: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "" "Datum en tijd heeft het verkeerde formaat. Gebruik één van de volgende " "formaten: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "" "Tijd heeft een verkeerd formaat. Gebruik één van de volgende formaten: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Geef een geheel getal in." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Zorg ervoor dat deze waarde minder of gelijk is aan %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Zorg ervoor dat deze waarde groter of gelijk is aan %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" waarde dient een float te zijn." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Geef een getal in." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Zorg ervoor dat er niet meer dan %s nummers in totaal zijn." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Zorg ervoor dat er niet meer dan %s plaatsen na de comma zijn." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Zorg ervoor dat er niet meer dan %s nummers voor de comma staan." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "Er was geen bestand aangegeven. Bekijken het type encoding in het formulier." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Er was geen bestand aangegeven." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Het gegeven bestand is leeg." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -180,13 +184,13 @@ msgstr "" "Zorg ervoor dat deze bestandsnaam maximaal %(max)d tekens lang is (de naam " "heeft %(length)d tekens)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Gelieve ofwel een bestand mee te geven ofwel de checkbox aan te tikken, niet " "beide." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -194,181 +198,178 @@ msgstr "" "Upload een geldige afbeelding. Het bestand dat je hebt geuploadet was ofwel " "een afbeelding ofwel een corrupte afbeelding." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Pagina is niet 'last', noch kan het omgezet worden naar een int." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Ongeldige pagina (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Ongeldige definitie van permissie." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Ongeldige pk '%s' - object bestaat niet." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Incorrect type. Pk waarde werd verwacht, maar %s gekregen." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Object met %s=%s bestaat niet." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Ongeldige hyperlink - Geen URL match" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Ongeldige hyperlink - Incorrecte URL match" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Ongeldige hyperlink door configuratiefout" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Ongeldige hyperlink - object bestaat niet." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Incorrect type. Url string werd verwacht, maar %s gekregen." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Ongeldige data" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Geen input gegeven" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Kan geen nieuw item aanmaken, enkel bestaande items mogen bijgewerkt worden." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Verwachtte een lijst van items." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Niet gevonden" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Toestemming geweigerd" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Server applicatie fout" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Verbindingsfout." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Slecht gevormde request." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Incorrecte authenticatie gegevens." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Authenticatie gegevens werden niet gegeven." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Je hebt geen toestemming om deze actie te ondernemen." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Methode '%s' is niet toegestaan." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Kon niet voldoen aan de Accept header van de request" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Niet ondersteund media type '%s' in de request." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Request werd gethrottled." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Verwachtte beschikbaarheid in %d second%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Onverwachte fout" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Niet gevonden." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Methode niet ondersteund voor dit endpoint." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Verkeerde argumenten." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Data validatie fout" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Integriteitsfout voor verkeerde of ongeldige argumenten" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Preconditie fout" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Fout in filter params types." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' moet een integer waarde zijn." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "tags" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -423,7 +424,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -435,26 +436,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" Taiga Support:\n" -" %(support_url)s\n" -"
\n" -" Contacteer ons:\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Mailing lijst:\n" -" \n" -" %(mailing_list_url)s\n" -" \n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -506,103 +487,88 @@ msgstr "" " Commentaar: %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "We hadden minstens één rol nodig" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Dump file nodig" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Ongeldig dump formaat" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" niet gevonden in dit project" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Ongeldige inhoud. Volgend formaat geldt {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Het bevat ongeldige eigen velden:" - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Naam gedupliceerd voor het project" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "fout bij het importeren van project data" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "fout bij importeren rollen" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "fout bij importeren lidmaatschappen" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "fout bij importeren van project attributenlijst" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "fout bij importeren van standaard projectattributen waarden" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "fout bij importeren eigen attributen" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "fout bij importeren sprints" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "fout bij importeren user stories" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "fout bij importeren taken" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "fout bij importeren issues" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "fout bij importeren user stories" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "fout bij importeren taken" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "fout bij importeren wiki pagina's" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "fout bij importeren wiki links" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "fout bij importeren tags" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "fout bij importeren tijdlijnen" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Fout bij genereren project dump" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -622,15 +588,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Fout bij laden project dump" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -803,77 +769,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Je project dump is geïmporteerd" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" niet gevonden in dit project" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Ongeldige inhoud. Volgend formaat geldt {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Het bevat ongeldige eigen velden:" + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Naam gedupliceerd voor het project" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "naam" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "omschrijving" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "volledige naam" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "e-mail adres" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "commentaar" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "aanmaakdatum" @@ -904,7 +890,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Extra info" @@ -938,507 +924,577 @@ msgstr "" "\n" "[Taiga] Feedback van %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "De payload is geen geldige json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Het project bestaat niet" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Slechte signature" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "Het element waarnaar verwezen wordt bestaat niet" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "De status bestaat niet" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Status veranderd door Bitbucket commit" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Ongeldige issue informatie" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Ongeldige issue commentaar informatie" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Ongeldige issue informatie" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Status veranderd door GitHub commit." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Issue aangemaakt via GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Commentaar via GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Het element waarnaar verwezen wordt bestaat niet" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Status veranderd door GitLab commit" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "De status bestaat niet" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Aangemaakt via GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Bekijk project" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Bekijk milestones" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Bekijk user stories" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Bekijk taken" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Bekijk issues" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Bekijk wiki pagina's" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Bekijk wiki links" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Vraag lidmaatschap aan" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Voeg user story toe aan project" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Voeg commentaar toe aan user stories" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Voeg commentaar toe aan taken" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Voeg issues toe" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Voeg commentaar toe aan issues" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Voeg wiki pagina toe" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Wijzig wiki pagina" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Voeg wiki link toe" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Wijzig wiki link" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Voeg milestone toe" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Wijzig milestone" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Verwijder milestone" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Bekijk user story" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Voeg user story toe" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Wijzig user story" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Verwijder user story" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Voeg taak toe" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Wijzig taak" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Verwijder taak" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Voeg issue toe" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Wijzig issue" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Verwijder issue" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Voeg wiki pagina toe" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Wijzig wiki pagina" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Verwijder wiki pagina" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Voeg wiki link toe" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Wijzig wiki link" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Verwijder wiki link" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Wijzig project" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Voeg lid toe" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Verwijder lid" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Verwijder project" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Voeg lid toe" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Verwijder lid" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Admin project waarden" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Admin rollen" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "eigenaar" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Onvolledige argumenten" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Ongeldig afbeelding formaat" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Ongeldige template naam" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Ongeldige template omschrijving" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Je hebt geen toestamming om dat te bekijken." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "Project ID van object is niet gelijk aan die van het project" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "project" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "inhoud type" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "object id" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "gemodifieerde datum" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "bijgevoegd bestand" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "is verouderd" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "volgorde" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 -msgid "Text" +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" msgstr "" #: taiga/projects/custom_attributes/choices.py:28 -msgid "Multi-Line Text" +msgid "Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:29 -msgid "Date" +msgid "Multi-Line Text" msgstr "" #: taiga/projects/custom_attributes/choices.py:30 +msgid "Date" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "type" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "waarden" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "user story" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "taak" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "issue" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Er bestaat er al één met dezelfde naam." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "status" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "onderwerp" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "kleur" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "toegewezen aan" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "is requirement van de klant" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "is requirement van het team" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Commentaar is al verwijderd" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Commentaar niet verwijderd" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Verander" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Creëer" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Verwijder" @@ -1494,7 +1550,7 @@ msgstr "verwijderd" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Niet toegewezen" @@ -1541,97 +1597,77 @@ msgstr "Van:" msgid "To:" msgstr "Naar:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "inhoud" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "geblokkeerde notitie" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Je hebt geen toestemming om deze sprint op deze issue te zetten." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Je hebt geen toestemming om deze status toe te kennen aan dze issue." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "" "Je hebt geen toestemming om dit ernstniveau toe te kennen aan deze issue." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "" "Je hebt geen toestemming om deze prioriteit toe te kennen aan deze issue." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Je hebt geen toestemming om dit type toe te kennen aan deze issue." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "status" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "erstniveau" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "prioriteit" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "milestone" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "datum van afwerking" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "onderwerp" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "toegewezen aan" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "externe referentie" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Vind ik leuk" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Personen die dit leuk vinden" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1643,8 +1679,9 @@ msgstr "geschatte start datum" msgid "estimated finish date" msgstr "geschatte datum van afwerking" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "is gesloten" @@ -1656,120 +1693,132 @@ msgstr "beschikbaarheid" msgid "The estimated start must be previous to the estimated finish." msgstr "The geschatte start moet vroeger zijn dan het geschatte einde." -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Er is geen sprint met dat id" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "is geblokkeerd" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' parameter is verplicht" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project' parameter is verplicht" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "aangemaakt op" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "uitnodiging extra text" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "gebruiker volgorde" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "The gebruikers is al lid van het project" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "standaard punten" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "standaard US status" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "standaard punten" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "default taak status" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "standaard prioriteit" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "standaard ernstniveau" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "standaard issue status" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "standaard issue type" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "leden" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "totaal van de milestones" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "totaal story points" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "actief backlog paneel" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "actief kanban paneel" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "actief wiki paneel" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "actief issues paneel" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "videoconference systeem" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "aanmaak template" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "is privé" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "anonieme toestemmingen" @@ -1777,169 +1826,251 @@ msgstr "anonieme toestemmingen" msgid "user permissions" msgstr "gebruikers toestemmingen" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "is privé" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "tag kleuren" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "gewijzigde datum en tijd" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "module config" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "is gearchiveerd" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "kleur" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "work in progress limiet" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "waarde" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "standaard rol eigenaar" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "standaard instellingen" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "us statussen" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "punten" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "taak statussen" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "issue statussen" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "issue types" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "prioriteiten" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "ernstniveaus" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "rollen" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "aanmaak datum en tijd" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "geschiedenis items" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "verwittig gebruikers" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Verwittiging bestaat voor gespecifieerde gebruiker en project" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2441,141 +2572,135 @@ msgstr "" "\n" "[%(project)s] Wiki Pagina verwijderd \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Volgers bevat ongeldige gebruikers" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "De versie moet een integer zijn" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "De versie stemt niet overeen met de huidige waarde" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "versie" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "E-mail adres is al in gebruik" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Ongeldige rol voor project" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Standaard opties" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Status van User story" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Punten" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Statussen van taken" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Statussen van Issues" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Types van issue" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Prioriteiten" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Ernstniveaus" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Rollen" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Toekomstige sprint" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Project einde" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Token is ongeldig" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "tags" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "tag kleuren" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "" @@ -2591,9 +2716,35 @@ msgstr "takenbord volgorde" msgid "is iocaine" msgstr "is iocaine" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Er is geen taak met dat id" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -2947,12 +3098,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -2970,12 +3121,12 @@ msgstr "" "gebruikers" #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -2988,303 +3139,388 @@ msgstr "" "definitie tot taak tot het afleveren naar de klant." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Nieuw" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Klaar" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "Lopende" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Klaar om te testen" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Afgewerkt" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Gearchiveerd" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Gesloten" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Info nodig" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Verzet naar later" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Geweigerd" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Bug" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Vraag" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Verbetering" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Laag" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normaal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Hoog" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Wensenlijst" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Mineur" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Belangrijk" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Kritiek" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Product Owner" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "backlog volgorde" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "sprint volgorde" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "afwerkdatum" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "is requirement van de klant" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "is requirement van het team" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "gegenereerd van issue" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Er is geen user story met dat id" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Er is geen project met dat is" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Er is geen user story status met dat id" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "E-mail adres is al in gebruik" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Er is geen taak status met dat id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Ongeldige rol voor project" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Standaard opties" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Status van User story" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Punten" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Statussen van taken" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Statussen van Issues" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Types van issue" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioriteiten" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Ernstniveaus" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Rollen" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Stemmen" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Stem" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'inhoud' parameter is verplicht" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parameter is verplicht" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "gebruiker met laatste wijziging" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3312,52 +3548,52 @@ msgstr "" msgid "Important dates" msgstr "Belangrijke data" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Gedupliceerde e-mail" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Ongeldige e-mail" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Ongeldige gebruikersnaam of e-mail" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Mail met succes verzonden!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Huidig wachtwoord parameter vereist" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Nieuw wachtwoord parameter vereist" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Ongeldige lengte van wachtwoord, minstens 6 tekens vereist" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Ongeldig huidig wachtwoord" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "Ongeldig, weet je zeker dat het token correct en ongebruikt is?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Ongeldig, weet je zeker dat het token correct is?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "superuser status" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3365,24 +3601,24 @@ msgstr "" "Beduidt dat deze gebruik alle toestemmingen heeft zonder deze expliciet toe " "te wijzen." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "gebruikersnaam" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Vereist. 30 of minder karakters. Letters, nummers en /./-/_ karakters" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Geef een geldige gebruikersnaam in" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "actief" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3390,71 +3626,63 @@ msgstr "" "Beduidt of deze gebruiker als actief moet behandeld worden. Deselecteer dit " "i.p.v. accounts te verwijderen." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografie" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "foto" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "toetrededatum" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "standaard taal" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "standaard tijdzone" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "kleur tags" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "e-mail token" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "nieuw e-mail adres" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "toestemmingen" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "ongeldig" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Ongeldige gebruikersnaam. Probeer met een andere." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker." @@ -3577,48 +3805,52 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Je bent getaiganiseerd!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Er is geen rol met dat id" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "ongeldig" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Ongeldige gebruikersnaam. Probeer met een andere." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" "Gedupliceerde key value overtreed unieke constraint. Key '{}' bestaat al." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "key" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "geheime sleutel" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "status code" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "request data" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "request headers" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "response data" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "response headers" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "duur" diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po index f7ce189e..38f5cfc9 100644 --- a/taiga/locale/pl/LC_MESSAGES/django.po +++ b/taiga/locale/pl/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/pl/)\n" @@ -22,153 +22,157 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Publiczna rejestracja jest wyłączona" -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "Nieprawidłowy typ rejestracji" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "Nieprawidłowy typ logowania" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Nazwa użytkownika jest już używana." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Ten adres email jest już w użyciu." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Token nie zgadza się z żadnym zaproszeniem" + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Użytkownik już zarejestrowany" + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Błąd przy tworzeniu użytkownika." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Nieprawidłowy token" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "Nieprawidłowa nazwa użytkownika" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Wymagane. Maksymalnie 255 znaków. Litery, cyfry oraz /./-/_ " -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Nazwa użytkownika jest już używana." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "Ten adres email jest już w użyciu." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Token nie zgadza się z żadnym zaproszeniem" - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Użytkownik już zarejestrowany" - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Błąd przy tworzeniu użytkownika." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Nieprawidłowy token" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "To pole jest wymagane." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Nieprawidłowa wartość." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "'%s' wartość musi przyjąć True albo False," -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Podaj prawidłowy 'slug' zawierający litery, cyfry, podkreślenia lub myślniki." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Dokonał właściwego wyboru. Wartość %(value)s nie jest jedną z dostępnych " "opcji." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Podaj właściwy adres email." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "Zły format. Użyj jednego z tych formatów: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "Zły format. Użyj jednego z tych formatów: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "Zły format. Użyj jednego z tych formatów: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Wpisz cały numer" -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Upewnij się, że wartość jest mniejsza lub równa od %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Upewnij się, że wartość jest większa lub równa od %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" wartość musi być zmiennoprzecinkowa." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Wpisz numer." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Upewnij się że nie podałeś więcej niż %s znaków." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Upewnij się, że nie ma więcej niż %s miejsc po przecinku." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Upewnij się, że nie ma więcej niż %s cyfr przed przecinkiem." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Plik nie został wysłany. Sprawdź kodowanie znaków w formularzu." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Plik nie został wysłany." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Wysłany plik jest pusty." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -176,11 +180,11 @@ msgstr "" "Upewnij się, że nazwa pliku ma maksymalnie %(max)d znaków.(Ilość znaków to: " "%(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "Proszę wybrać jedną z opcji, nie obie." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -188,182 +192,179 @@ msgstr "" "Prześlij właściwy obraz. Plik który próbujesz przesłać nie jest obrazem lub " "jest uszkodzony." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Strona nie jest ostatnią i nie może zostać zmieniona na int." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Niewłaściwa strona (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Nieprawidłowa definicja uprawnień." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Nieprawidłowa wartość klucza '%s' -Obiekt nie istniej." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Niepoprawny typ. Oczekiwana wartość, otrzymana %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Obiekt z %s=%s nie istnieje." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Nieprawidłowy odnośnik - brak pasującego URL" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Nieprawidłowy odnośnik - źle dopasowany URL" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Nieprawidłowy odnośnik z powodu błędu konfiguracji" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Nieprawidłowy odnośnik - obiekt nie istnieje." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Niepoprawny typ. Oczekiwany url, otrzymany %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Nieprawidłowa dana" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Nic nie wpisano" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Nie można utworzyć nowego obiektu, tylko istniejące obiekty mogą być " "aktualizowane." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Oczekiwana lista elementów." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Nie znaleziono" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Dostęp zabroniony" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Błąd aplikacji serwera" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Błąd połączenia." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Błędne żądanie." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Nieprawidłowe dane uwierzytelniające." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Nie podano danych uwierzytelniających." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Nie masz uprawnień do wykonania tej czynności." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Metoda %s nie dozwolona." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Nie udało się spełnić żądania Accept Header" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Niewspierany typ pliku '%s' w żądaniu." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Żądanie zostało zduszone." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Oczekiwana dostępność w ciągu %d sekund%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Nieoczekiwany błąd" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Nie odnaleziono." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Metoda nie wspierana dla tej końcówki." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Złe argumenty." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Błąd walidacji dancyh" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Błąd integralności dla błędnych lub nieprawidłowych argumentów" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Błąd warunków wstępnych" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Błąd w parametrach typów filtrów." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' musi być wartością typu int." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "tagi" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -418,7 +419,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -430,26 +431,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" Pomoc Taiga:\n" -" %(support_url)s\n" -"
\n" -" Skontaktuj się z " -"nami:\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Lista mailingowa:" -"\n" -" \n" -" %(mailing_list_url)s\n" -" \n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -507,103 +488,88 @@ msgstr "" " Komentarz: %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Potrzeba conajmiej jednej roli" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Wymagany plik zrzutu" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Nieprawidłowy format zrzutu" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" nie odnaleziono w projekcie" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Niewłaściwa zawartość. Musi to być {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Zawiera niewłaściwe pola niestandardowe." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Nazwa projektu zduplikowana" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "błąd w trakcie importu danych projektu" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "błąd w trakcie importu ról" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "błąd w trakcie importu członkostw" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "błąd w trakcie importu atrybutów projektu" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "błąd w trakcie importu domyślnych atrybutów projektu" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "błąd w trakcie importu niestandardowych atrybutów" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "błąd w trakcie importu sprintów" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "błąd w trakcie importu historyjek użytkownika" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "błąd w trakcie importu zadań" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "błąd w trakcie importu zgłoszeń" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "błąd w trakcie importu historyjek użytkownika" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "błąd w trakcie importu zadań" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "błąd w trakcie importu stron Wiki" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "błąd w trakcie importu linków Wiki" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "błąd w trakcie importu tagów" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "błąd w trakcie importu osi czasu" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Błąd w trakcie generowania zrzutu projektu" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -623,15 +589,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Błąd w trakcie wczytywania zrzutu projektu" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -871,77 +837,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Twój zrzut projektu został prawidłowo zaimportowany" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" nie odnaleziono w projekcie" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Niewłaściwa zawartość. Musi to być {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Zawiera niewłaściwe pola niestandardowe." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Nazwa projektu zduplikowana" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nazwa" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "opis" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Następny url" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "użytkownik" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "aplikacja" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "Imię i Nazwisko" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "adres e-mail" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "komentarz" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "data utworzenia" @@ -972,7 +958,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Dodatkowe info" @@ -1006,547 +992,577 @@ msgstr "" "\n" "[Taiga] Informacje od %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "Źródło nie jest prawidłowym plikiem json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Projekt nie istnieje" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Błędna sygnatura" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "Element referencyjny nie istnieje" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Status nie istnieje" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Status zmieniony przez commit z BitBucket" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Nieprawidłowa informacja o zgłoszeniu" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"Zgłoszenie utworzone przez [@{bitbucket_user_name}]({bitbucket_user_url} " -"\"Zobacz profil użytkownika @{bitbucket_user_name}'s \") na BitBucket.\n" -"Źródłowe zgłoszenie z BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Idź do 'bb#{number} - {subject}'\"):\n" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" "\n" -"{description}" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Zgłoszenie utworzone przez BitBucket." - -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Nieprawidłowa informacja o komentarzu do zgłoszenia" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -"Skomentowane przez [@{bitbucket_user_name}]({bitbucket_user_url} \"Zobacz " -"profil użytkownika @{bitbucket_user_name}'s\") na BitBucket.\n" -"Źródłowe zgłoszenie z BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Idź do 'bb#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Nieprawidłowa informacja o zgłoszeniu" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Komentarz z BitBucket:\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Status zmieniony przez [@{github_user_name}]({github_user_url} \"Zobacz " -"profil użytkownika @{github_user_name}'s \") z commitu na GitHub " -"[{commit_id}]({commit_url} \"Zobacz commit'{commit_id} - " -"{commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Status zmieniony przez commit z GitHub" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Zgłoszenie utworzone przez [@{github_user_name}]({github_user_url} \"Zobacz " -"profil użytkownika @{github_user_name}'s \") na GitHub.\n" -"Źródłowe zgłoszenie z GitHub: [gh#{number} - {subject}]({github_url} \"Idź " -"do 'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Zgłoszenie utworzone przez GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"Skomentowane przez [@{github_user_name}]({github_user_url} \"Zobacz profil " -"użytkownika @{github_user_name}'s GitHub profile\") na GitHub.\n" -"Źródłowe zgłoszenie z GitHub: [gh#{number} - {subject}]({github_url} \"Idź " -"do 'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Komentarz z GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Element referencyjny nie istnieje" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Status zmieniony przez commit z GitLab" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Status nie istnieje" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Utworzone przez GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Skomentowane przez [@{gitlab_user_name}]({gitlab_user_url} \"Zobacz profil " -"użytkownika @{gitlab_user_name}'s \") na GitLab.\n" -"Źródłowe zgłoszenie z: [gl#{number} - {subject}]({gitlab_url} \"Idź do " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Komentarz z GitLab:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Zobacz projekt" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Zobacz kamienie milowe" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Zobacz historyjki użytkownika" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Zobacz zadania" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Zobacz zgłoszenia" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Zobacz strony Wiki" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Zobacz linki Wiki" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Poproś o członkowstwo" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Dodaj historyjkę użytkownika do projektu" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Dodaj komentarze do historyjek użytkownika" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Dodaj komentarze do zadań" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Dodaj zgłoszenia" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Dodaj komentarze do zgłoszeń" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Dodaj strony Wiki" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Modyfikuj stronę Wiki" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Dodaj link do Wiki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modyfikuj link do Wiki" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Dodaj kamień milowy" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modyfikuj Kamień milowy" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Usuń kamień milowy" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Zobacz historyjkę użytkownika" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Dodaj historyjkę użytkownika" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modyfikuj historyjkę użytkownika" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Usuń historyjkę użytkownika" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Dodaj zadanie" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modyfikuj zadanie" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Usuń zadanie" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Dodaj zgłoszenie" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modyfikuj zgłoszenie" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Usuń zgłoszenie" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Dodaj strony Wiki" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Modyfikuj stronę Wiki" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Usuń stronę Wiki" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Dodaj link do Wiki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modyfikuj link do Wiki" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Usuń link Wiki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Modyfikuj projekt" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Dodaj członka zespołu" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Usuń członka zespołu" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Usuń projekt" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Dodaj członka zespołu" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Usuń członka zespołu" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Administruj wartościami projektu" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Administruj rolami" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "właściciel" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Pola niekompletne" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Niepoprawny format obrazka" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Nieprawidłowa nazwa szablonu" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Nieprawidłowy opis szablonu" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Nie masz uprawnień by to zobaczyć." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "ID nie pasuje pomiędzy obiektem a projektem" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "projekt" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "typ zawartości" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "id obiektu" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "data modyfikacji" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "załączony plik" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "jest przestarzałe" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "kolejność" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Niestandardowy" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Tekst" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Teks wielowierszowy" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "typ" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "wartości" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "historyjka użytkownika" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "zadanie" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "zgłoszenie" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Już istnieje jeden z taką nazwą." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "status" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "temat" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "kolor" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "przypisane do" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "wymaganie klienta" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "wymaganie zespołu" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Komentarz został już usunięty" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Komentarz nie został usunięty" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Zmień" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Utwórz" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Usuń" @@ -1602,7 +1618,7 @@ msgstr "usuniete" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Nieprzypisane" @@ -1649,95 +1665,75 @@ msgstr "Od:" msgid "To:" msgstr "Do:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "zawartość" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "zaglokowana notatka" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Nie masz uprawnień do połączenia tego zgłoszenia ze sprintem." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Nie masz uprawnień do ustawienia statusu dla tego zgłoszenia." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Nie masz uprawnień do ustawienia ważności dla tego zgłoszenia." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "Nie masz uprawnień do ustawienia priorytetu dla tego zgłoszenia." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Nie masz uprawnień do ustawienia typu dla tego zgłoszenia." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "status" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "ważność" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "priorytet" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "kamień milowy" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "data zakończenia" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "temat" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "przypisane do" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "źródło zgłoszenia" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1749,8 +1745,9 @@ msgstr "szacowana data rozpoczecia" msgid "estimated finish date" msgstr "szacowana data zakończenia" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "jest zamknięte" @@ -1762,120 +1759,132 @@ msgstr "dostępność" msgid "The estimated start must be previous to the estimated finish." msgstr "Szacowana data rozpoczęcia musi być wcześniejsza niż data zakończenia." -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Nie ma sprintu o takim ID" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "jest zablokowane" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' parametr jest obowiązkowy" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project' parametr jest obowiązkowy" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "utwórz na" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "dodatkowy tekst w zaproszeniu" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "kolejność użytkowników" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "Użytkownik już jest członkiem tego projektu" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "domyślne punkty" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "domyślny status dla HU" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "domyślne punkty" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "domyślny status dla zadania" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "domyślny priorytet" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "domyślna ważność" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "domyślny status dla zgłoszenia" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "domyślny typ dla zgłoszenia" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "członkowie" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "wszystkich kamieni milowych" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "wszystkich punktów " -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "aktywny panel backlog" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "aktywny panel Kanban" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "aktywny panel Wiki" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "aktywny panel zgłoszeń " -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "system wideokonferencji" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "dodatkowe dane dla wideokonferencji" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "szablon " -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "jest prywatna" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "uprawnienia anonimowych" @@ -1883,169 +1892,251 @@ msgstr "uprawnienia anonimowych" msgid "user permissions" msgstr "uprawnienia użytkownika" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "jest prywatna" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "kolory tagów" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "data aktualizacji" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "ilość" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "konfiguracja modułów" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "zarchiwizowane" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "kolor" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "limit postępu prac" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "wartość" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "domyśla rola właściciela" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "domyślne opcje" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "statusy HU" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "pinkty" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "statusy zadań" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "statusy zgłoszeń" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "typy zgłoszeń" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "priorytety" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "ważność" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "role" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "data utworzenia" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "wpisy historii" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "powiadom użytkowników" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Obserwowane" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Powiadomienie istnieje dla określonego użytkownika i projektu" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Nieprawidłowa wartość dla poziomu notyfikacji" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2778,142 +2869,136 @@ msgstr "" "\n" "[%(project)s] Usunął stronę Wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Obserwatorzy zawierają niepoprawnych użytkowników" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "Wersja musi być integerem ;)" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "Parametr wersji jest nieprawidłowy" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Podana wersja nie zgadza się z aktualną." -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "wersja" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Tena adres e-mail jest już w użyciu" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Nieprawidłowa rola w projekcie" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Domyślne opcje" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Statusy historyjek użytkownika" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Punkty" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Statusy zadań" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Statusy zgłoszeń" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Typu zgłoszeń" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Priorytety" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Ważność" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Role" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Przyszły sprint" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Zakończenie projektu" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Nieprawidłowy token." -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "tagi" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "kolory tagów" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "Nie masz uprawnień do ustawiania sprintu dla tego zadania." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" "Nie masz uprawnień do ustawiania historyjki użytkownika dla tego zadania" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania" @@ -2929,9 +3014,35 @@ msgstr "Kolejność tablicy zadań" msgid "is iocaine" msgstr "Iokaina" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Nie ma zadania z takim ID" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3313,12 +3424,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3336,12 +3447,12 @@ msgstr "" "klienta." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3353,305 +3464,390 @@ msgstr "" "wyświetlane dla klienta a członkowie zespołu wyciągają je z kolejki." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Nowe" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Gotowe" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "W toku" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Gotowe do testów" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Gotowe!" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Zarchiwizowane" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Zamknięte" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Potrzebne informacje" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Odroczone" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Odrzucone" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Błąd" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Pytanie" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Ulepszenie" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Niski" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normalny" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Wysoki" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Życzenie" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Pomniejsze" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Istotne" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Krytyczne" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Właściciel produktu" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Interesariusz" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Nie masz uprawnień do ustawiania sprintu dla tej historyjki użytkownika." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "Nie masz uprawnień do ustawiania statusu do tej historyjki użytkownika." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rola" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "Kolejność backlogu" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "kolejność sprintu" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "data zakończenia" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "wymaganie klienta" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "wymaganie zespołu" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "wygenerowane ze zgłoszenia" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Nie ma historyjki użytkownika z takim ID" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Nie ma projektu z takim ID" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Nie ma statusu historyjki użytkownika z takim ID" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "Tena adres e-mail jest już w użyciu" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Nie ma statusu zadania z takim ID" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Nieprawidłowa rola w projekcie" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Domyślne opcje" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Statusy historyjek użytkownika" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Punkty" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Statusy zadań" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Statusy zgłoszeń" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Typu zgłoszeń" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Priorytety" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Ważność" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Role" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Głosy" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Głos" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "Parametr 'zawartość' jest wymagany" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "Parametr 'id_projektu' jest wymagany" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "ostatnio zmodyfikowane przez" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Dla pełengo diffa sprawdź API historii" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3679,56 +3875,56 @@ msgstr "" msgid "Important dates" msgstr "Ważne daty" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Zduplikowany adres e-mail" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Niepoprawny adres e-mail" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Nieprawidłowa nazwa użytkownika lub adrs e-mail" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "E-mail wysłany poprawnie!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Należy podać bieżące hasło" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Należy podać nowe hasło" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "" "Nieprawidłowa długość hasła - wymagane jest co najmniej 6 znaków" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Podałeś nieprawidłowe bieżące hasło" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Niepoprawne, jesteś pewien, że token jest poprawny i nie używałeś go " "wcześniej? " -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Niepoprawne, jesteś pewien, że token jest poprawny?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "status SUPERUSER" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3736,24 +3932,24 @@ msgstr "" "Oznacza, że ten użytkownik posiada wszystkie uprawnienia bez konieczności " "ich przydzielania." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "nazwa użytkownika" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Wymagane. 30 znaków. Liter, cyfr i znaków /./-/_" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Wprowadź poprawną nazwę użytkownika" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "aktywny" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3761,71 +3957,63 @@ msgstr "" "Oznacza, że ten użytkownik ma być traktowany jako aktywny. Możesz to " "odznaczyć zamiast usuwać konto." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "zdjęcie" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "data dołączenia" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "domyślny język Taiga" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "domyślny szablon Taiga" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "domyśla strefa czasowa" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "kolory tagów" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "tokem e-mail" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "nowy adres e-mail" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "uprawnienia" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "Niepoprawne" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Nazwa użytkownika lub hasło są nieprawidłowe" @@ -4017,47 +4205,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Zostałeś zaTaigowany" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Nie istnieje rola z takim ID" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "Niepoprawne" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "Duplikowanie wartości klucza. Klucz '{}' już istnieje." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "klucz" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "sekretny klucz" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "kod statusu" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "data żądania" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "nagłówki żądań" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "dane odpowiedzi" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "nagłówki odpowiedzi" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "czas trwania" diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po index df5cf8cc..aaa0f7bc 100644 --- a/taiga/locale/pt_BR/LC_MESSAGES/django.po +++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po @@ -22,9 +22,9 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-06-13 01:32+0000\n" -"Last-Translator: Mairieli Wessel \n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" +"Last-Translator: Taiga Dev Team \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/pt_BR/)\n" "MIME-Version: 1.0\n" @@ -33,152 +33,156 @@ msgstr "" "Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Registro público está desabilitado. " -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "tipo de registro inválido" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "tipo de login inválido" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Nome de usuário já está em uso." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Este e-mail já está em uso." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Esse token não bate com nenhum convite." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Este usuário já está registrado." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "O usuário já é membro do projeto." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Erro ao criar um novo usuário." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Token inválido" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "nome de usuário inválido" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Obrigatório. No máximo 255 caracteres. Letras, números e /./-/_ ." -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Nome de usuário já está em uso." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "Este e-mail já está em uso." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Esse token não bate com nenhum convite." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Este usuário já está registrado." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "O usuário já é membro do projeto." - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Erro ao criar um novo usuário." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Token inválido" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Este campo é obrigatório." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Valor inválido." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "O valor de '%s' deve ser ou True ou False." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Entre uma 'slug' válida, consistindo de letras, números, underscores ou " "hífens." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Escolha uma alternativa válida. %(value)s não está disponível." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Preencha com um e-mail válido." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "A data está no formato errado. Use um desses no lugar: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "Formato da data e hora errado. Use um destes: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "Hora com formato errado. Use um destes: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Insira um número inteiro." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Garanta que o valor é menor ou igual a %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Garanta que o valor é maior ou igual a %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "O valor de \"%s\" deve ser decimal (float)." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Insira um número." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Garanta que não há mais que %s dígitos no total." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Garanta que não há mais que %s casas decimais." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Garanta que não há mais que %s dígitos antes do ponto decimal." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Nenhum arquivo enviado. Verifique o tipo de codificação no formulário." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Nenhum arquivo enviado." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "O arquivo enviado está vazio." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -186,11 +190,11 @@ msgstr "" "Garanta que o nome do arquivo tem no máximo %(max)d caracteres (no momento " "tem %(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "Envie um arquivo ou marque o checkbox \"vazio\", não ambos." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -198,182 +202,179 @@ msgstr "" "Envie uma imagem válida. O arquivo que você mandou ou não era uma imagem ou " "está corrompido." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Elemento bloqeado" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Página não é \"última\", nem pode ser convertída para um inteiro." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Página inválida (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Definição de permissão inválida." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Chave primária '%s' inválida - objeto não existe." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Tipo incorreto. Esperado valor de chave primária, recebido %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Objeto com %s=%s não existe." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Hyperlink inválido - Nenhuma URL corresponde" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Hyperlink inválido - Corresponde a URL incorreta" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Hyperlink inválido devido a erro de configuração" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Hyperlink inválido - objeto não existe." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Tipo incorreto. Esperada string de url, recebido %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Dados inválidos" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Nenhuma entrada providenciada" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Não é possível criar um novo item, somente itens já existentes podem ser " "atualizados." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Esperada uma lista de itens." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Não encontrado" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Permissão negada" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Erro no servidor da aplicação" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Erro na conexão." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Requisição mal-formada" -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Credenciais de autenticação incorretas." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Credenciais de autenticação não informadas." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Você não possui permissão para executar esta ação." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Método '%s' não é permitido" -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Não foi possível satisfazer o cabeçalho Accept da requisição" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Tipo de mídia '%s' não suportado na requisição." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Requisição foi sujeita a limites." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Esperado disponível em %d segundo%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Erro inesperado" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Não encontrado." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Método não suportado por esse endpoint." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Argumentos errados." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Erro de validação dos dados" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Erro de Integridade para argumentos inválidos ou errados" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Erro de pré-condição" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Erro nos tipos de parâmetros do filtro." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'projeto' deve ser um valor inteiro." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "tags" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -428,7 +429,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -440,27 +441,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" Suporte Taiga:\n" -" %(support_url)s\n" -"
\n" -" Entre em contato:" -"\n" -" \n" -" %(support_email)s\n" -" \n" -"
\n" -" Lista de e-mail:" -"\n" -" \n" -" %(mailing_list_url)s\n" -" \n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -518,103 +498,88 @@ msgstr "" " Comentário: %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Nós precisamos de pelo menos uma função" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Necessário de arquivo de restauração" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Formato de aquivo de restauração inválido" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" não encontrado nesse projeto" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "conteúdo inválido. Deve ser {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Contém campos personalizados inválidos" - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Nome duplicado para o projeto" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "erro ao importar informações de projeto" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "erro importando funcões" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "erro importando filiações" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "erro importando lista de atributos do projeto" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "erro importando valores de atributos do projeto padrão" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "erro importando atributos personalizados" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "erro importando sprints" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "erro importando histórias de usuário" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "erro importando tarefas" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "erro importando problemas" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "erro importando histórias de usuário" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "erro importando tarefas" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "erro importando páginas wiki" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "erro importando wiki links" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "erro importando tags" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "erro importando linha do tempo" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "erro inesperado ao importar projeto" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Erro gerando arquivo de restauração do projeto" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -656,15 +621,15 @@ msgstr "" "MAIS INFORMAÇÕES DO ERRO:\n" "------------" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Erro carregando arquivo de restauração do projeto" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "Erro ao carregar arquivo de restauração do projeto" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "-- sem informações detalhadas --" @@ -903,77 +868,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] A restauração do seu projeto foi importada" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" não encontrado nesse projeto" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "conteúdo inválido. Deve ser {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Contém campos personalizados inválidos" + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Nome duplicado para o projeto" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Autenticação necessária" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "Nome" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "Ícone da url" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "descrição" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Próxima url" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "chave secreta para cifrar os tokens da aplicação" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "usuário" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "aplicação" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "nome completo" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "endereço de e-mail" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "comentário" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "data de criação" @@ -1004,7 +989,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Informação extra" @@ -1038,389 +1023,345 @@ msgstr "" "\n" "[Taiga] Resposta de %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "A carga não é um json válido" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "O projeto não existe" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Assinatura Ruim" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "O elemento referenciado não existe" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "O estatus não existe" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Status alterado em Bitbucket commit" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Informação de problema inválida" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"Problema criado por [@{bitbucket_user_name}]({bitbucket_user_url} \"Veja " -"profile do BitBucket de @{bitbucket_user_name}\") a partir do BitBucket.\n" -"Origem BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Ir para " -"'bb#{number} - {subject}'\"):\n" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" "\n" -"{description}" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Problema criado via Bitbucket." - -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Informação de comentário de problema inválida" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -"Comentário por [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Informação de problema inválida" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Comentário pelo Bitbucket:\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Status alterado por [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Status alterado por commit do Github." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Problema criado por [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Problema criado pelo Github." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"Comentário por [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Comentário pelo Github:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "O elemento referenciado não existe" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Status alterado por um commit de Gitlab" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "O estatus não existe" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Criado pelo Gitlab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Comentário por [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Comentário pelo GitLab:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Ver projeto" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Ver marco de progresso" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Ver histórias de usuário" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Ver tarefa" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Ver problemas" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Ver página wiki" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Ver links wiki" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Solicitar filiação" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Adicionar história de usuário em projeto" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Adicionar comentários em histórias de usuário" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Adicionar comentário para tarefa" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Adicionar problemas" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Adicionar comentários aos problemas" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Adicionar página wiki" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "modificar página wiki" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Adicionar link wiki" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modificar wiki link" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Adicionar marco de progresso" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modificar marco de progresso" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Remover marco de progresso" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Ver história de usuário" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Adicionar história de usuário" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modificar história de usuário" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Apagar história de usuário" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Adicionar tarefa" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modificar tarefa" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Deletar tarefa" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Adicionar problema" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modificar problema" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Deletar problema" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Adicionar página wiki" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "modificar página wiki" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Deletar página wiki" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Adicionar link wiki" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modificar wiki link" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Deletar link wiki" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Modificar projeto" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Adicionar membro" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Remover membro" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Deletar projeto" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Adicionar membro" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Remover membro" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Valores projeto admin" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Funções Admin" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "dono" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Argumentos incompletos" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Formato de imagem inválida" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Nome de template inválido" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Descrição de template inválida" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "Id de usuário inválido" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "O usuário não existe" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "O usuário deve ser um membro do projeto" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" @@ -1428,158 +1369,233 @@ msgstr "" "O projeto deve ter um dono e pelo menos um dos usuários precisa ser um " "administrador ativo" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Você não tem permissão para ver isso" -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Atualizações parciais não são suportadas" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "ID do projeto não combina entre objeto e projeto" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "projeto" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "tipo de conteúdo" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "identidade de objeto" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "data modificação" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "arquivo anexado" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "está obsoleto" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "ordem" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "Aparece em" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Personalizado" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "Este projeto está bloqueado por problemas de pagamento" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "Este projeto está bloqueado por um administrador" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "Este projeto está bloqueado porque o proprietário deixou o projeto" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Texto" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Multi-linha" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Data" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "Tipo" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "valores" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "história de usuário" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "tarefa" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "problema" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Já existe um com o mesmo nome." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "status" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "assunto" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "cor" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "assinado a" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "É requerimento do cliente" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "É requerimento do time" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Comentário já apagado" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Comentário não apagado" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Alterar" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Criar" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Apagar" @@ -1635,7 +1651,7 @@ msgstr "removido" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Não-atribuído" @@ -1682,96 +1698,76 @@ msgstr "De:" msgid "To:" msgstr "Para:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "conteúdo" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "nota bloqueada" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Você não tem permissão para colocar essa sprint para esse problema." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Você não tem permissão para colocar esse status para esse problema." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Você não tem permissão para colocar essa gravidade para esse problema." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "" "Você não tem permissão para colocar essa prioridade para esse problema." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Você não tem permissão para colocar esse tipo para esse problema." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "status" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "severidade" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "prioridade" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "marco de progresso" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "data de término" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "assunto" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "assinado a" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "referência externa" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Curtir" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Curtidas" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1783,8 +1779,9 @@ msgstr "data de início estimada" msgid "estimated finish date" msgstr "data de encerramento estimada" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "está fechado" @@ -1796,120 +1793,132 @@ msgstr "disponibilidade" msgid "The estimated start must be previous to the estimated finish." msgstr "A estimativa de inicio deve ser anterior a estimativa de encerramento" -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Não há sprint com esse id" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "está bloqueado" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' parametro é mandatório" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project' parametro é mandatório" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "email" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "criado em" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "texto extra de convite" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "ordem de usuário" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "O usuário já é membro do projeto" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "pontos padrão" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "status de US padrão" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "pontos padrão" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "status padrão de tarefa" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "prioridade padrão" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "severidade padrão" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "status padrão de problema" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "tipo padrão de problema" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "logotipo" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "membros" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "total de marcos de progresso" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "pontos totais de US" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "painel de backlog ativo" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "painel de kanban ativo" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "painel de wiki ativo" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "painel de problemas ativo" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "sistema de vídeo conferência" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "informação extra de vídeo conferência" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "template de criação" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "é privado" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "permissão anônima" @@ -1917,169 +1926,251 @@ msgstr "permissão anônima" msgid "user permissions" msgstr "permissão de usuário" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "é privado" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "é destaque" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "está procurando colaboradores" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "cores de tags" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "data de atualização" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "contagem" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "atividades da última semana" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "atividades do último mês" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "atividades do último ano" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "configurações de módulos" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "está arquivado" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "cor" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "trabalho no limite de progresso" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "valor" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "função padrão para dono " -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "opções padrão" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "status de US" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "pontos" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "status de tarefa" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "status de problemas" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "tipos de problema" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "severidades" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "funções" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Envolvido" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Tudo" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Nada" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "data de criação" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "histórico de entradas" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "notificar usuário" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Observado" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Existe notificação para usuário e projeto especifcado" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Valor inválido para nível de notificação" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2792,148 +2883,141 @@ msgstr "" "\n" "[%(project)s] Apagou a página Wiki \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Observadores contém usuários inválidos" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "A versão precisa ser um inteiro" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "O parâmetro da versão não é válido" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "A versão não corresponde com a atual" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "versão" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" "Você não pode deixar o projeto se você é o dono é não há outros " "administradores" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Endereço de e-mail já utilizado" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Função inválida para projeto" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." -msgstr "O dono do projeto deve ser um administrador." - -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -"Pelo menos one dos usuários deve ser um administrador ativo neste projeto." -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Opções padrão" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Status de história de usuário" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Pontos" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Status de tarefas" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Status de problemas" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Tipos de problemas" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Prioridades" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Severidades" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Funções" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "Você atingiu o seu limite atual de membros para projetos privados" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "Você atingiu o seu limite atual de membros para projetos públicos" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "Você não pode ter mais projetos privados" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" "Este projeto atingiu o seu limite atual de membros para projetos privados" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "Você não pode ter mais projetos públicos" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" "Este projeto atingiu o seu limite atual de membros para projetos públicos" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Sprint futuro" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Fim do projeto" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Token é inválido" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "Token expirou" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "tags" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "cores de tags" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "Você não tem permissão para colocar esse sprint para essa tarefa." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" "Você não tem permissão para colocar essa história de usuário para essa " "tarefa." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "Você não tem permissão para colocar esse status para essa tarefa." @@ -2949,9 +3033,35 @@ msgstr "ordenar por quadro de tarefa" msgid "is iocaine" msgstr "é Iocaine" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Não há tarefas com esse id" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3344,12 +3454,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3365,12 +3475,12 @@ msgstr "" "se no processo que é compreendido sobre o produto e seus clientes." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3384,307 +3494,393 @@ msgstr "" "uma lista." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Novo" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Pronto" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "Em andamento" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Pronto para teste" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Terminado" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Arquivado" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Fechado" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Precisa de informação" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Adiado" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Rejeitado" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Bug" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Pergunta" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Melhoria" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Baixa" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Alta" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Desejável" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Secundário" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Importante" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Crítica" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Front" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Back" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Product Owner" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Você não tem permissão para colocar esse sprint para essa história de " "usuário." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "Você não tem permissão para colocar esse status para essa história de " "usuário." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Gerando a história de usuário #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "função" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "ordem do backlog" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "ordem do sprint" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "data de término" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "É requerimento do cliente" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "É requerimento do time" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "Gerado do problema" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Não há história de usuário com esse id" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Não há projeto com esse id" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Não há status de história de usuário com este id" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "Endereço de e-mail já utilizado" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Não há status de tarega com este id" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Função inválida para projeto" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "O dono do projeto deve ser um administrador." + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" +"Pelo menos one dos usuários deve ser um administrador ativo neste projeto." + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Opções padrão" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Status de história de usuário" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Pontos" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Status de tarefas" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Status de problemas" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Tipos de problemas" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioridades" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Severidades" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Funções" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Votos" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Vote" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "parâmetro 'conteúdo' é mandatório" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "parametro 'project_id' é mandatório" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "último modificador" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Verifique o histórico da API para a exata diferença" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "Membro do Projeto" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "Membros do Projeto" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "id" @@ -3712,54 +3908,54 @@ msgstr "Restrições" msgid "Important dates" msgstr "Datas importantes" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "E-mail duplicado" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Não é um e-mail válido" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Usuário ou e-mail inválido" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "E-mail enviado com sucesso" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Parâmetro de senha atual necessário" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Parâmetro de nova senha necessário" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Comprimento de senha inválido, pelo menos 6 caracteres necessários" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Senha atual inválida" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Inválido, você está certo que o token está correto e não foi usado " "anteriormente?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Inválido, tem certeza que o token está correto?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "status de superuser" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3767,24 +3963,24 @@ msgstr "" "Designa que esse usuário tem todas as permissões sem explicitamente assiná-" "las" -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "usuário" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Requerido. 30 caracteres ou menos. Letras, números e caracteres /./-/_" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Digite um usuário válido" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "ativo" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3792,71 +3988,63 @@ msgstr "" "Designa quando esse usuário deve ser tratado como ativo. desmarque isso em " "vez de deletar contas." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografia" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "foto" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "data ingressado" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "lingua padrão" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "tema padrão" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "fuso horário padrão" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "tags coloridas" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "token de e-mail" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "novo endereço de email" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "permissões" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "inválido" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Usuário inválido. Tente com um diferente." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Usuário ou senha não correspondem ao usuário" @@ -4044,48 +4232,52 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Você foi Taigatizado!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Não há função com esse id" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "inválido" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Usuário inválido. Tente com um diferente." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" "Valor de chave duplicada viola regra de limitação. Chave '{}' já existe." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "chave" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "chave secreta" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "código de status" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "dados da requisição" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "cabeçalhos da requisição" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "dados de resposta" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "cabeçalhos de resposta" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "duração" diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po index d4164285..a70669de 100644 --- a/taiga/locale/ru/LC_MESSAGES/django.po +++ b/taiga/locale/ru/LC_MESSAGES/django.po @@ -16,9 +16,9 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-07-26 04:45+0000\n" -"Last-Translator: Egor Poderyagin \n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" +"Last-Translator: Taiga Dev Team \n" "Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/ru/)\n" "MIME-Version: 1.0\n" @@ -29,157 +29,161 @@ msgstr "" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Публичная регистрация отключена." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "неправильный тип регистрации" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "неправильный тип логина" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Это имя уже используется." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Этот адрес почты уже используется." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Токен не подходит ни под одно корректное приглашение." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Пользователь уже зарегистрирован." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Этот пользователь уже является участником данного проекта" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Ошибка при создании нового пользователя." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Неверный токен" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "неправильное имя пользователя" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Обязательно. 255 символов или меньше. Буквы, числа и символы /./-/_" -#: 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:94 -msgid "Token not matches any valid invitation." -msgstr "Токен не подходит ни под одно корректное приглашение." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Пользователь уже зарегистрирован." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "Этот пользователь уже является участником данного проекта" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Ошибка при создании нового пользователя." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Неверный токен" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Это поле обязательно." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Неправильное значение." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "значение '%s' должно быть True - верно - или False - ложно." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Введите корректное 'ссылочное имя' состоящее из букв, чисел, подчёркиваний и " "дефисов." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Выберите правильное значение. %(value)s не является одним из доступных " "значений." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Введите правильный адрес email." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "Дата имеет неверный формат. Воспользуйтесь одним из этих форматов: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "" "Дата и время имеют неправильный формат. Воспользуйтесь одним из этих " "форматов: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "" "Время имеет неправильный формат. Воспользуйтесь одним из этих форматов: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Введите целое число." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Убедитесь, что это значение меньше или равно %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Убедитесь, что это значение больше или равно %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" значение должно быть числом с плавающей точкой." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Введите число." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Убедитесь, что здесь всего не больше %s цифр." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Убедитесь, что здесь не больше %s цифр после точкой." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Убедитесь, что здесь не больше %s цифр перед точкой." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Файл не был отправлен. Проверьте тип кодировки на форме." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Файл не был отправлен." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Отправленный файл пуст." -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -187,11 +191,11 @@ msgstr "" "Убедитесь, что имя этого файла имеет не больше %(max)d букв (сейчас - " "%(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "Пожалуйста, или отправьте файл, или снимите флажок." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -199,181 +203,178 @@ msgstr "" "Загрузите корректное изображение. Файл, который вы загрузили - либо не " "изображение, либо не корректное изображение." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Заблокированный элемент" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Страница не является 'последней' и не может быть приведена к int." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Неправильная страница (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Неправильное определение разрешения" -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Неправильное значение ключа '%s' - объект не существует." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Неверный тип. Ожидалось значение ключа, пришло %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Объект с %s=%s не существует." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Неправильная гиперссылка - нет подходящего URL" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Неправильная гиперссылка - URL не подходит" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Неправильная гиперссылка из-за ошибки конфигурации" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Неправильная ссылка - объект не существует." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Неверный тип. Ожидалась строка URL, получено %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Неправильные данные." -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Ввод отсутствует" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Нельзя создать новые объект, только существующие объекты могут быть изменены." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Ожидался список объектов." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Не найдено" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Доступ запрещён" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Ошибка приложения на сервере" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Ошибка соединения." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Неверно сформированный запрос." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Неверные данные для аутентификации." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Данные для аутентификации не предоставлены." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "У вас нет разрешения для этого действия." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Метод '%s' не разрешён." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Не удалось соответствовать заголовку принятия для этого запроса" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Не поддерживаемый тип медиа '%s' в запросе." -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Запрос был замят" -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Будет доступно в течение %d секунд%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Неожиданная ошибка" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Не найдено." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Метод не поддерживается с этого конца." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Неправильные аргументы." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Ошибка при проверке данных" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Ошибка целостности из-за неправильных параметров" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Ошибка предусловия" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "Не осталось места для проектов" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Ошибка в типах фильтров для параметров." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' должно быть целым значением." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "тэги" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -428,7 +429,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -440,27 +441,6 @@ msgid "" " \n" " " msgstr "" -"\n" -" ПоддержкаTaiga:\n" -" %(support_url)s\n" -"
\n" -" Свяжитесь с нами:" -"\n" -"
\n" -" %(support_email)s\n" -" \n" -"
\n" -" Рассылка:\n" -" \n" -" %(mailing_list_url)s\n" -" \n" -" " #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -518,103 +498,88 @@ msgstr "" " Комментарий: %(comment)s\n" " " -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Нам была нужна хотя бы одна роль" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Необходим дамп" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Неправильный формат дампа" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" не найдено в этом проекте" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Неправильные данные. Должны быть в формате {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Содержит неверные специальные поля" - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Уже есть такое имя для проекта" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "ошибка при импорте данных проекта" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "ошибка при импорте ролей" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "ошибка при импорте членства" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "ошибка при импорте списков свойств проекта" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "ошибка при импорте значений по умолчанию свойств проекта" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "ошибка при импорте пользовательских свойств" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "ошибка при импорте спринтов" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "ошибка импорта историй от пользователей" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "ошибка импорта задач" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "ошибка при импорте запросов" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "ошибка импорта историй от пользователей" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "ошибка импорта задач" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "ошибка при импорте вики-страниц" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "ошибка при импорте вики-ссылок" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "ошибка импорта тэгов" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "ошибка импорта хронологии проекта" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "неожиданная ошибка импортирования проекта" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Ошибка создания свалочного файла для проекта" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -649,15 +614,15 @@ msgstr "" "ТРАССИРОВКА ОШИБКИ:\n" "------------" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Ошибка загрузки дампа" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "Ошибка загрузки дампа вашего проекта" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "-- нет детальной информации --" @@ -894,77 +859,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Дамп вашего проекта импортирован" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" не найдено в этом проекте" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Неправильные данные. Должны быть в формате {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Содержит неверные специальные поля" + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Уже есть такое имя для проекта" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Необходима аутентификация" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "имя" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "url иконки" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "веб" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "описание" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Следующий url" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "секретный ключ для шифрования токенов приложения" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "пользователь" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "приложение" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "полное имя" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "адрес email" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "комментарий" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "дата создания" @@ -995,7 +980,7 @@ msgstr "" " " #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Дополнительное инфо" @@ -1029,390 +1014,345 @@ msgstr "" "\n" "[Taiga] Отзыв от %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "Нагрузочный файл не является правильным json-файлом" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Проект не существует" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Плохая подпись" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: 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:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Статус не существует" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Статус изменён из-за вклада с BitBucket" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Неверная информация о запросе" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"Запрос создан [@{bitbucket_user_name}]({bitbucket_user_url} \"Посмотреть " -"профиль @{bitbucket_user_name} на BitBucket\") на BitBucket.\n" -"Изначальный запрос на BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Перейти к 'bb#{number} - {subject}'\"):\n" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" "\n" -"{description}" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Запрос создан из BitBucket." - -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Неправильная информация в комментарии к запросу" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -"Комментарий от [@{bitbucket_user_name}]({bitbucket_user_url} \"Посмотреть " -"профиль @{bitbucket_user_name} на BitBucket\") на BitBucket.\n" -"Изначальный запрос на BitBucket: [bb#{number} - {subject}]({bitbucket_url} " -"\"Перейти к 'bb#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Неверная информация о запросе" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Комментарий от BitBucket:\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Статус изменён пользователем [@{github_user_name}]({github_user_url} " -"\"Посмотреть профиль @{github_user_name} на GitHub\") из-за вклада на GitHub " -"[{commit_id}]({commit_url} \"Посмотреть вклад '{commit_id} - " -"{commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Статус изменён из-за вклада на GitHub." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"Запрос создана [@{github_user_name}]({github_user_url} \"Посмотреть профиль " -"@{github_user_name} на GitHub\") из GitHub.\n" -"Исходный запрос на GitHub: [gh#{number} - {subject}]({github_url} \"Перейти " -"к 'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Запрос создан из GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"Комментарий от [@{github_user_name}]({github_user_url} \"Посмотреть профиль " -"@{github_user_name} на GitHub\") из GitHub.\n" -"Исходный запрос на GitHub: [gh#{number} - {subject}]({github_url} \"Перейти " -"к 'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"Комментарий из GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Указанный элемент не существует" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Статус изменён из-за вклада на GitLab" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Статус не существует" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Создано из GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Комментарий от [@{gitlab_user_name}]({gitlab_user_url} \"Посмотреть профиль " -"@{gitlab_user_name} на GitLab\") из GitLab.\n" -"Исходный запрос на GitLab: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Комментарий из GitLab:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Просмотреть проект" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Просмотреть вехи" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Просмотреть пользовательские истории" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Просмотреть задачи" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Посмотреть запросы" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Просмотреть wiki-страницы" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Просмотреть wiki-ссылки" -#: 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:75 -msgid "Add wiki page" -msgstr "Создать wiki-страницу" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Изменить wiki-страницу" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Добавить wiki-ссылку" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Изменить wiki-ссылку" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Добавить веху" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Изменить веху" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Удалить веху" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Просмотреть пользовательскую историю" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Добавить пользовательскую историю" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Изменить пользовательскую историю" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Удалить пользовательскую историю" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Добавить задачу" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Изменить задачу" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Удалить задачу" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Добавить запрос" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Изменить запрос" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Удалить запрос" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Создать wiki-страницу" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Изменить wiki-страницу" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Удалить wiki-страницу" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Добавить wiki-ссылку" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Изменить wiki-ссылку" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Удалить wiki-ссылку" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Изменить проект" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Добавить участника" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Удалить участника" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Удалить проект" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Добавить участника" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Удалить участника" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Управлять значениями проекта" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Управлять ролями" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "владелец" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Список аргументов неполон" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Неправильный формат изображения" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Неверное название шаблона" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Неверное описание шаблона" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "Неправильный id пользователя" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "Пользователь не существует" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "Пользователь должен быть участником проекта" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" @@ -1420,158 +1360,233 @@ msgstr "" "У проекта должен быть владелец и по крайней мере один пользователь должен " "быть активным администратором" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "У вас нет разрешения на просмотр." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Частичные обновления не поддерживаются" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "Идентификатор проекта не подходит к этому объекту" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "проект" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "тип содержимого" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "идентификатор объекта" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "изменённая дата" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "приложенный файл" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "устаревшее" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "порядок" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Специальный" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "Проект заблокирован из-за ошибки при оплате" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "Проект заблокирован администраторами" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "Проект заблокирован, потому-что владелец ушёл" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Текст" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Многострочный текст" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Дата" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "тип" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "значения" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "пользовательская история" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "задача" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "запрос" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Это имя уже используется." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "Ссылка" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "cтатус" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "тема" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "цвет" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "назначено" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "является требованием клиента" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "является требованием команды" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Комментарий уже был удалён" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Комментарий не удалён" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Изменить" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Создать" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Удалить" @@ -1627,7 +1642,7 @@ msgstr "удалено" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Не назначено" @@ -1674,99 +1689,79 @@ msgstr "От:" msgid "To:" msgstr "Кому:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "содержимое" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "Заметка о блокировке" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "спринт" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "" "У вас нет прав для того чтобы установить такой спринт для этого запроса" -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "" "У вас нет прав для того чтобы установить такой статус для этого запроса" -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "" "У вас нет прав для того чтобы установить такую важность для этого запроса" -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "" "У вас нет прав для того чтобы установить такой приоритет для этого запроса" -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "У вас нет прав для того чтобы установить такой тип для этого запроса" -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "Ссылка" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "cтатус" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "важность" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "приоритет" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "веха" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "дата завершения" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "тема" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "назначено" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "внешняя ссылка" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Лайк" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Лайки" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "ссылочное имя" @@ -1778,8 +1773,9 @@ msgstr "предполагаемая дата начала" msgid "estimated finish date" msgstr "предполагаемая дата завершения" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "закрыто" @@ -1793,120 +1789,132 @@ msgstr "" "Предполагаемая дата начала должна предшествовать предполагаемой дате " "завершения." -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Не существует спринта с таким идентификатором" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "заблокировано" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "параметр '{param}' является обязательным" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "параметр 'project' является обязательным" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "электронная почта" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "создано" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "идентификатор" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "дополнительный текст к приглашению" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "порядок пользователей" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "Этот пользователем уже является участником проекта" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "очки по умолчанию" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "статусы ПИ по умолчанию" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "очки по умолчанию" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "статус задачи по умолчанию" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "приоритет по умолчанию" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "важность по умолчанию" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "статус запроса по умолчанию" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "тип запроса по умолчанию" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "лготип" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "участники" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "общее количество вех" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "очки истории" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "активная панель списка задач" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "активная панель kanban" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "активная wiki-панель" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "панель активных запросов" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "система видеоконференций" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "дополнительные данные системы видеоконференций" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "шаблон для создания" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "личное" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "права анонимов" @@ -1914,169 +1922,251 @@ msgstr "права анонимов" msgid "user permissions" msgstr "права пользователя" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "личное" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "особенность" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "ищут людей" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "ищем замечания людей" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "цвета тэгов" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "токен передачи проекта" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "заблокированный код" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "дата и время обновления" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "количество" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "фанатов на прошлой недели " -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "фанатов в прошлом месяце" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "фанатов в прошлом году" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "активность за неделю" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "активность за месяц" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "активность за год" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "конфигурация модулей" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "архивировано" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "цвет" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "ограничение на активную работу" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "значение" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "роль владельца по умолчанию" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "параметры по умолчанию" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "статусы ПИ" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "очки" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "статусы задач" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "статусы запросов" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "типы запросов" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "приоритеты" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "степени важности" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "роли" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Вовлеченные" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Все" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Никаких" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "дата и время создания" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "записи истории" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "уведомить пользователей" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Просмотренные" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Уведомление существует для данных пользователя и проекта" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Неверное значение для уровня уведомлений" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2795,145 +2885,137 @@ msgstr "" "\n" "[%(project)s] Удалена вики-страница \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "наблюдатели содержат неправильных пользователей" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "Версия должна быть целым значением" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "Значение версии некорректно" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Версия не соответствует текущей" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "версия" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" "Вы не можете покинуть проект, если вы владелец или нет других администраторов" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "Этот почтовый адрес уже используется" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Неверная роль для этого проекта" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." -msgstr "Владелец проекта должен быть администратором" - -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -"По крайней мере один пользователь должен быть администратором для этого " -"проекта" -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Параметры по умолчанию" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Статусу пользовательских историй" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Очки" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Статусы задачи" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Статусы запроса" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Типы запроса" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Приоритеты" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Степени важности" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Роли" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "Вы достигли лимита участников для частного проекта" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "Вы достигли лимита участников для публичного проекта" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "Вы не можете иметь больше частных проектов" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "В этом частном проекте достигнут лимит участников" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "Вы не можете иметь больше публичных проектов" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "В этом публичном проекте достигнут лимит участников" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Будущий спринт" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Окончание проекта" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Неверный токен" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "Срок действия токена истёк" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "тэги" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "цвета тэгов" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "У вас нет прав, чтобы назначить этот спринт для этой задачи." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "" "У вас нет прав, чтобы назначить эту историю от пользователя этой задаче." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "У вас нет прав, чтобы установить этот статус для этой задачи." @@ -2949,9 +3031,35 @@ msgstr "порядок панели задач" msgid "is iocaine" msgstr "- иокаин" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Нет задачи с таким идентификатором" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3412,12 +3520,12 @@ msgstr "" "[%(project)s] Предложение передачи проекта\n" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3434,12 +3542,12 @@ msgstr "" "известно о продукте и его пользователях." #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3453,305 +3561,392 @@ msgstr "" "задачи из очереди." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Новая" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Готово" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "В процессе" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Можно проверять" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Завершена" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Архивирована" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Закрыта" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Требуются подробности" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Отложено" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Отклонена" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Ошибка" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Вопрос" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Улучшение" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Низкий" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Обычный" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Высокий" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Список пожеланий" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Низкий" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Важный" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Критический" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "Юзабилити" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Дизайнер" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Фронтенд разработчик" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Бэкенд разработчик" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Владелец продукта" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Заинтересованная сторона" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "У вас нет прав чтобы установить спринт для этой пользовательской истории." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "У вас нет прав чтобы установить статус для этой пользовательской истории." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Генерируется пользовательская история #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "роль" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "порядок списка задач" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "порядок спринтов" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "дата окончания" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "является требованием клиента" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "является требованием команды" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "создано из запроса" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Не существует пользовательской истории с таким идентификатором" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Не существует проекта с таким идентификатором" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Не существует статуса пользовательской истории с таким идентификатором" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "Этот почтовый адрес уже используется" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Не существует статуса задачи с таким идентификатором" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Неверная роль для этого проекта" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "Владелец проекта должен быть администратором" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" +"По крайней мере один пользователь должен быть администратором для этого " +"проекта" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Параметры по умолчанию" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Статусу пользовательских историй" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Очки" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Статусы задачи" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Статусы запроса" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Типы запроса" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Приоритеты" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Степени важности" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Роли" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Голоса" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Голосовать" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "параметр 'content' является обязательным" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "параметр 'project_id' является обязательным" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "последний отредактировавший" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Свертесть с историей API для получения изменений" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "Участник проекта" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "Участники проекта" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "id" @@ -3779,145 +3974,137 @@ msgstr "Ограничения" msgid "Important dates" msgstr "Важные даты" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "Этот email уже используется" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Невалидный email" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Неверное имя пользователя или e-mail" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Письмо успешно отправлено!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Поле \"текущий пароль\" является обязательным" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Поле \"новый пароль\" является обязательным" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Неверная длина пароля, требуется как минимум 6 символов" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Неверно указан текущий пароль" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "Неверно, вы уверены что токен правильный и не использовался ранее?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Неверно, вы уверены что токен правильный?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "статус суперпользователя" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "Выбранный пользователь имеет все разрешения, ему не чего назначит." -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "имя пользователя" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "Обязательно. 30 символов или меньше. Буквы, числа и символы /./-/_" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Введите корректное имя пользователя." -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "активный" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "Выбранный пользователь активен. Отменить выбор для удаления аккаунта." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "биография" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "фотография" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "когда присоединился" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "язык по умолчанию" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "тема по умолчанию" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "временная зона по умолчанию" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "установить цвета для тэгов" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "email токен" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "новый email адрес" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "максимальное число частных проектов" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "максимальное число публичных проектов" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "максимальное число участников для каждого частного проекта" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "максимальное число участников для каждого публичного проекта" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "разрешения" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "невалидный" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Неверное имя пользователя. Попробуйте другое." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Имя пользователя или пароль не соответствуют пользователю." @@ -4111,48 +4298,52 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Вы в Тайге!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Не существует роли с таким идентификатором" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "невалидный" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Неверное имя пользователя. Попробуйте другое." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" "Дублирующий ключ, значение должно быть уникальны. Ключ '{}' уже существует." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "ключ" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "Секретный ключ" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "код статуса" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "данные запроса" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "заголовки запроса" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "данные ответа" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "заголовки ответа" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "длительность" diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po index 705d5cc6..b8f0cec5 100644 --- a/taiga/locale/sv/LC_MESSAGES/django.po +++ b/taiga/locale/sv/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/sv/)\n" @@ -19,154 +19,158 @@ msgstr "" "Language: sv\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "Publikt register är avvaktiverad." -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "Felaktigt registertyp" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "Invalid inloggningstyp" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Användarnamnet används redan" + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "E-postadressen används redan" + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Förekomsten passar inte invitationen. " + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Användaren finns redan." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Ett fel uppstod når användaren skapades. " + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Felaktig förekomst. " + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "Felaktigt användarnamn" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "Kräver färre än 255 tecken. Kan vara tecken, nummer och /./-/_." -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Användarnamnet används redan" - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "E-postadressen används redan" - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Förekomsten passar inte invitationen. " - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Användaren finns redan." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Ett fel uppstod når användaren skapades. " - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Felaktig förekomst. " - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Fältet är obligatoriskt." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Felaktigt värde. " -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "'%s' värdet måste vara sann eller falskt. " -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Skriv in ett giltigt 'slugg' som består av bokstäver, nummer, understreck " "och bindestreck." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Välj korrekt. %(value)s är inte ett giltigt val. " -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Skriv in en giltig e-postadress" -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "Felaktigt datumformat. Använd ett av dessa formaten istället: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "Tidsdatum har fel format. Bruk ett av dessa formaten istället: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "Felaktigt tidsformat. Bruk ett av dessa formaten istället: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Skriv ett helt nummer." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Försäkra dig om att värdet är mindre eller lika med %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Försäkra dig om att värdet är större eller lika med %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" värde måste vara flyttal." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Skriv in ett nummer." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Försäkra dig om att det inge är mera än %s siffror i totalen. " -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Försäkra dig om att det inte är mera än %s decimaler." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "" "Försäkra dig om det inte är mera än %s siffror till vänster om " "decimalpunkten." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Inga filer skickades. Check kodningstypen på formularet. " -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Skickade ingen fil. " -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "Den insända filen är tom. " -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -174,12 +178,12 @@ msgstr "" "Försäkra dig om att filnamnet har som mest %(max)d tecken (det har " "%(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Vänligen lämna in en fil eller kontrollera kryssrutan för klar, inte båda." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -187,182 +191,179 @@ msgstr "" "Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild " "eller en skadad bild." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "" "Sidan är inte \"sist\", och inte heller kan den omvandlas till ett heltal." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Felaktig sida (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Ogiltigt definition för behörighet." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Ogiltigt paket '%s' - objektet existerar inte." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Ogiltigt typ. Förväntad paketvärde, mottaget %s. " -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "Objekt med %s=%s existerar inte. " -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Ogiltigt länkadress - Inga länkar passar." -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Ogiltigt länkadress - Felaktig matchning av länkar." -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Felaktig länk förorsakad av et konfigurationsfel. " -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Fel länk - objekten existerar inte. " -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Felaktigt typ. Förväntad länksträng, mottagit %s. " -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Felaktigt data" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Inga indata" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "" "Det går inte att skapa ett nytt objekt, endast befintliga poster uppdateras." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Förväntad lista på poster." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Hittade inte" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "Du har inte behöriget" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Serverprogramfel." -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Felaktigt förbindelse." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Felaktigt begäran" -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Felaktiga autentiseringsreferenser " -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Autentiseringsuppgifter lämnades inte." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Du har inte behörigheter för att utföra denna åtgärd. " -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "Metoden '%s' är inte tillåtet. " -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "Det gick inte att tillgodose begäran på Accept-huvudet" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "Mediatypen '%s' du begär stöds inte. " -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "Begäran blev strypt." -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "Förväntas bli tillgängligt inom %d second%s." -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Oväntat fel" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Hittade inget" -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "Metoden stöds inte för denna slutpunkten." -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Fel argument." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Datavalideringsfel" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Integritetsfel för felaktiga eller ogiltiga argument" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Förutsättningsfel" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Fel i filterparametertyper." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'Projektet\" måste vara ett heltal." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "taggar" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -417,7 +418,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -429,22 +430,6 @@ msgid "" " \n" " " msgstr "" -"\n" -"Taiga Support:\n" -"" -"%(support_url)s\n" -"
\n" -"Kontakt oss:\n" -"\n" -"%(support_email)s\n" -"\n" -"
\n" -"E-postlista:\n" -"\n" -"%(mailing_list_url)s\n" -"" #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -491,103 +476,88 @@ msgid "" " " msgstr "" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "Vi behöver minst en roll" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "Behöver en hämtningsfil" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Invalid hämtningsfilformat" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" gick inte att hitta för det här projektet" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Felaktigt innehåll. Det måste vara {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Innehåller felaktigt anpassad fält." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Namnet är upprepad för projektet" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "fel vid import av projektdata" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "fel vid importering av roller" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "fel vid import av medlemskap" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "fel vid import av en lista på projektegenskaper" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "fel vid import av standard projektegenskapsvärden" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "fel vid import av anpassade egenskaper" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "felaktig import av sprintar" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "fel vid import av användarhistorier" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "fel vid import av uppgifter" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "fel vid import av ärenden" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "fel vid import av användarhistorier" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "fel vid import av uppgifter" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "vel vid import av wiki-sidor" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "fel vid import av wiki-länkar" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "fel vid importering av taggar" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "fel vid importering av tidslinje" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Fel vid skapandet av projektkopia" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -607,15 +577,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Feil vid hämtning av projektkopia" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -766,77 +736,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Ditt projekt importerades korrekt" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" gick inte att hitta för det här projektet" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Felaktigt innehåll. Det måste vara {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Innehåller felaktigt anpassad fält." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Namnet är upprepad för projektet" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Verifiering krävs" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "namn" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "Ikonlänk" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "Internet" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "beskrivning" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Nästa länk" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "hemlig nyckel för kryptering av programtecken " -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "användare" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "program" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "hela namnet" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "e-postadress" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "kommentera" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "skapad datum" @@ -859,7 +849,7 @@ msgid "" msgstr "" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Extra information" @@ -885,515 +875,577 @@ msgid "" "[Taiga] Feedback from %(full_name)s <%(email)s>\n" msgstr "" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "Datasträngen är inte korrekt json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Projektet existerar inte" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Dålig signatur" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "Referenselementet existerar inte" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Statusen existerar inte" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Status ändrad från BitBucket skrivs in" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Felaktig ärendeinformation" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Ärende skapades från BitBucket." +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Felaktigt kommentarinformation för ärendet" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Felaktig ärendeinformation" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Status ändrad av [@{github_user_name}]({github_user_url} \"Se " -"@{github_user_name}'s GitHub profil\") från GitHub commit [{commit_id}]" -"({commit_url} \"Se bidrag '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Status ändrad från GitHub inläggs." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "Ärende skapad från GitHub." - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Referenselementet existerar inte" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "Status ändrad från GitLab inlagd" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Statusen existerar inte" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "Skapad från GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -"Kommentar av [@{gitlab_user_name}]({gitlab_user_url} \"Se " -"@{gitlab_user_name}'s GitLab profile\") från GitLab.\n" -"\n" -"Ursprunglig GitLab ärende: [gl#{number} - {subject}]({gitlab_url} \"Gå till " -"'gl#{number} - {subject}'\")\n" -"\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Visa projekt" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Visa milstolper" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Visa användarhistorier" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Visa uppgifter" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Visa ärenden" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Visa wiki-sidor" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Visa wiki-länkar" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Ansöka om medlemskap" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Lägg till användarhistorie till projekt" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Lägg till kommentarer till användarhistorie" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Lägg till kommentar till uppgifter" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Lägg till ärenden" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Lägg till kommentar till ärender" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Lägg till en wiki-sida" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Modifiera wiki-sida" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Lägg till wiki-länk" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Modifiera wiki-link" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Lägg till milstolpe" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Modifiera milstolpe" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Ta bort milstolpe" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Visa användarhistorie" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Lägg till användarhistorie" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Modifiera användarhistorien" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Ta bort användarhistorien" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Lägg till uppgift" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Modifiera uppgift" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Ta bort uppgift" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Lägg till ärende" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Modifiera ärende" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Ta bort ärende" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Lägg till en wiki-sida" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Modifiera wiki-sida" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Ta bort wiki-sida" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Lägg till wiki-länk" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Modifiera wiki-link" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Ta bort wiki-länk" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Mofifiera projekt" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Lägg till medlem" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Ta bort medlem" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Ta bort projekt" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Lägg till medlem" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Ta bort medlem" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Administrera projektvärden" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Administratorroller" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "ägare" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Felaktiga argument" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Felaktigt bildformat" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Inget giltigt mallnamn" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Inte giltigt mallbeskrivning" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Du har inte behörighet att se det. " -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Delvisa uppdateringar stöds inte. " -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "Projekt-ID stämmer inte mellan objekt och projekt" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "projekt" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "innehållstyp" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "objekt-ID" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "ändrad datum" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "bifogad fil" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "undviks" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "sortera" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "Dyker upp i " -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Anpassa" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Text" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Text med flera rader" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Datum" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "typ" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "värden" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "Användarhistorie" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "uppgift" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "Ärende" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Existerar redan med samma namn. " -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "status" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "titel" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "färg" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "Tilldelad till" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "är ett beställarkrav" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "är ett krav från arbetsgruppen" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Kommentaren är redan borttagit. " -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Kommentaren är inte borttagit" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Ändra" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Skapa" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Ta bort" @@ -1449,7 +1501,7 @@ msgstr "borttaget" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Ej tilldelad" @@ -1496,95 +1548,75 @@ msgstr "Från:" msgid "To:" msgstr "Till:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "innehåll" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "blockerad notering" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Du har inte behörighet att sätta sprinten till det här ärendet." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Du har inte behörighet att sätta status till det här ärendet. " -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Du har inte behörighet att sätta allvarsgrad till det här ärendet. " -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "Du har inte behörighet att sätta prioriteten för det här ärendet. " -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Du har inte behörighet att lägga till typen till ärendet. " -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "status" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "Allvarsgrad" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "prioritet" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "milstolpe" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "färdig datum" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "titel" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "Tilldelad till" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "extern referens" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Gillar" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Gillar" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slugg" @@ -1596,8 +1628,9 @@ msgstr "Beräknad startdatum" msgid "estimated finish date" msgstr "Beräknad slutdato" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "är stängd" @@ -1609,120 +1642,132 @@ msgstr "disponerar" msgid "The estimated start must be previous to the estimated finish." msgstr "Beräknad startdatum måste vara tidigare än beräknad slutdatum. " -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Det finns ingen sprint med det här ID-numret" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "är blockerad" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' parameter är obligatoriskt" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project' parameter är obligatoriskt" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "e-post" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "skapa som" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "textsträng" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "Invitation - extra text" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "användarorder" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "Användaren är redan medlem i projekt" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "standardpoäng" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "standard US-poäng" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "standardpoäng" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "standard status för uppgift" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "standard prioritet" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "standard allvarsgrad" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "standard status för ärende" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "standard typ för ärende" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "medlemmar" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "totalt antal milstolpar" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "totalt antal historiepoäng" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "aktivt panel för inkorg" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "aktiv kanban-panel" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "aktiv wiki-panel" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "aktiv panel för ärenden" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "videokonferensssystem" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "videokonferens - extra data" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "mall skapas" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "är privat" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "anonyma rättigheter" @@ -1730,169 +1775,251 @@ msgstr "anonyma rättigheter" msgid "user permissions" msgstr "användarbehörigheter" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "är privat" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "färger för taggar" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "uppdaterad dato och tid" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "räkna" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "konfigurera moduler" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "är arkiverad" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "färg" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "begränsad arbete pågår" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "värde" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "ägarens standardroll" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "standard val" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "US statuser" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "poäng" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "statuser för uppgifter" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "status för ärenden" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "ärendentyper" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "prioriteter" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "allvarsgrad" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "roller" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Involverad" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Alla" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Ingen" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "skapad dato och tid" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "historienotat" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "notifiera användare" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "Visad" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Notifiering finns för användaren och projektet" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Felaktigt värde för notifieringen" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2364,141 +2491,135 @@ msgid "" "[%(project)s] Deleted the Wiki Page \"%(page)s\"\n" msgstr "" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "Listan på bevakare består av felaktiga användare" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "Versionen måste vara ett heltal" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "Versionsparametern är ogiltig" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Versionen stämmer inte med den aktuella versionen" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "version" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "E-postadressen är redan använd" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Fel roll for projektet" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Standardval" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Status för användarhistorien" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Poäng" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Status för uppgifter" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Status för ärenden" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Ärendetyper" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Prioritet" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Allvarsgrad" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Roller" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Framtidig sprint" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Projektslut" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Textsträngen är ogiltig" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "taggar" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "färger för taggar" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "Du har inte behörighet åt att sätta sprinten till en uppgift" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "Du har inte behörighet att sätta använderhistorien till en uppgift." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "Du har inte behörighet att sätta status till en uppgift. " @@ -2514,9 +2635,35 @@ msgstr "Sortera uppgiftstavlan" msgid "is iocaine" msgstr "är Iocaine" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Det är ingen uppgift med det ID-numret" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -2857,12 +3004,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -2878,12 +3025,12 @@ msgstr "" "man lär sig om produkten, funktioner och kunder. " #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -2897,306 +3044,391 @@ msgstr "" "uppdragskön." #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Ny" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Leveransklar" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "Pågående" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Klart till test" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Färdig" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Arkiverad" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Stängd" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Behöver information" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Uppskjutit" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Avslått" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Bugg" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Fråga" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "Förbättring" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Låg" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Hög" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "Önskelista" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "Mindre" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Viktig" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Kritiskt" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Design" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Framsida" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Baksida" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Produktägare" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Intressent" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Du har inte behörighet för att lägga sprinten till den här användarhistorien" -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "" "Du har inte behörighet till att sätta den här statusen till " "användarhistorien." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Skapar användarhistorie #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "roll" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "sortera inkorgen" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "sortera sprintar" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "färdig datum" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "är ett beställarkrav" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "är ett krav från arbetsgruppen" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "skapad från ärende" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Det är inga användarhistoria med det ID-numret" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Det är inga projekt med det ID-numret" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Det är inga användarhistoria-status med det ID-numret" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "E-postadressen är redan använd" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Det är inga uppgifter med det ID-numret" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Fel roll for projektet" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Standardval" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Status för användarhistorien" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Poäng" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Status för uppgifter" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Status för ärenden" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Ärendetyper" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioritet" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Allvarsgrad" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Roller" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Röster" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Rösta" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'content' parametern är obligatoriskt" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametern är obligatoriskt" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "senastste ändring" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "Kolla historie API för exakt skillnad" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3224,54 +3456,54 @@ msgstr "" msgid "Important dates" msgstr "Viktiga datum" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "E-post-dublett" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Ingen giltig e-postadress" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Ogiltigt användarnamn eller e-postadress" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "E-posten skickades korrekt" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "Parameter för nuvarande lösenord krävs" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Parameter för nytt lösenord krävs" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Felaktig längd på lösenord. Minst 6 alfanumeriska tecken krävs." -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "Fel lösenord" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Fel. Är du säker på att strängen är korrekt och att du inte har använt det " "tidigare?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Fel, är du säker på att textsträngen är korrekt? " -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "status för administratorn" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." @@ -3279,25 +3511,25 @@ msgstr "" "Anger om användaren har alla behörigheter utan att uttryckligen tilldela " "dem. " -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "användarnamn" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Obligatoriskt. 30 eller färre alfanumeriska tecken, bokstäver och /./-/_ . " -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Skriv in ett giltigt användarnamn" -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "aktiv" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -3305,71 +3537,63 @@ msgstr "" "Anger om användaren ska betraktas som aktiv. Avmarkera detta i stället för " "att ta bort kontot." -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biografi" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "foto" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "blev medlem datum" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "standardspråk" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "standardtema" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "standard tidzon" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "farglägg taggar" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "e-poststräng" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "ny e-postadress" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "behörigheter" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "felaktigt" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Användarnamn eller lösenord passar inte." @@ -3490,47 +3714,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Du har blivit Taiganiserad!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Det är inga roller med det ID-numret" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "felaktigt" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "Dublett-nyckelvärden bryter unik begränsning. Key \"{}\" finns redan." -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "nyckel" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "Länk" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "hemlig nyckel" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "statuskod" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "begär data" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "begär titel" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "responsdata" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "responstitel" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "varaktighet" diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po index 15ea255e..379339fc 100644 --- a/taiga/locale/tr/LC_MESSAGES/django.po +++ b/taiga/locale/tr/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/" "language/tr/)\n" @@ -21,159 +21,163 @@ msgstr "" "Language: tr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "" -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "geçersiz kayıt tipi" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "geçersiz giriş tipi" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Kullanıcı adı zaten kullanımda." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "E-posta zaten kullanımda." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Kupon geçerli hiç bir davetle uyuşmuyor." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Kullanıcı zaten kayıtlı." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Bu kullanızı halihazırda zaten projenin bir üyesi." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Geçersiz kupon" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "geçersiz kullanıcı adı" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "" "Zorunlu. 255 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri" -#: taiga/auth/services.py:75 -msgid "Username is already in use." -msgstr "Kullanıcı adı zaten kullanımda." - -#: taiga/auth/services.py:78 -msgid "Email is already in use." -msgstr "E-posta zaten kullanımda." - -#: taiga/auth/services.py:94 -msgid "Token not matches any valid invitation." -msgstr "Kupon geçerli hiç bir davetle uyuşmuyor." - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "Kullanıcı zaten kayıtlı." - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "Bu kullanızı halihazırda zaten projenin bir üyesi." - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "Yeni kullanıcı oluşturulurken hata meydana geldi." - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "Geçersiz kupon" - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "Bu alan zorunlu." -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "Geçersiz değer." -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "%s' değeri ya Doğru ya da Yanlış olmalıdır." -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Harfler, rakamlar, altçizgi ve kesme işaretinden oluşan geçerli bir 'satır' " "girin." -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Geçerli bir seçenek belirleyin. %(value)s değeri mevcut seçenekler arasında " "yok." -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "Geçerli bir e-posta adresi girin." -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "Tarih biçemi yanlış. Belirtilen biçemlerden birini kullanın: %s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "Tarih saat biçemi yanlış. Belirtilen biçemlerden birini kullanın: %s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "Zaman biçemi yanlış. Belirtilen biçemlerden birini kullanın: %s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "Bir tam sayı girin." -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" "Bu değerin %(limit_value)s değerine eşit ya da daha az olduğundan emin olun." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" "Bu değerin %(limit_value)s değerine eşit ya da daha fazla olduğundan emin " "olun." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" değeri kesirli bir sayı olmalıdır." -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "Bir sayın girin." -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Toplamda %s basamaktan fazla olmadığından emin olun." -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "%s ondalık değerinden fazla olmalıdığından emin olun." -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "" "Virgülden önceki rakamların %s basamaktan fazla olmadığından emin olun." -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "Dosya ibraz edilmedi. Formdan kodlama tipini kontrol edin." -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "Dosya ibraz edilmedi." -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "İbraz edilen dosya boş" -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -181,13 +185,13 @@ msgstr "" "Bu dosya adının en fazla %(max)d karakterden oluştuğundan (uzunluğunun " "%(length)d olduğundan) emin olun" -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" "Lütfen bir dosya ibraz edin ya da onay kutusunu seçmeyin, ikisini birden " "olmaz." -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -195,180 +199,177 @@ msgstr "" "Geçerli bir resim yükleyin. Yüklenen dosya ya bozulmuş bir resim ya da bir " "resim dosyası değil." -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Engellenmiş nesne" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "Sayfa 'last'(son) değil, tamsayıya da çevrilemiyor." -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "Geçersiz sayfa (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "Geçersiz izin tanımı." -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "Geçersiz pk '%s' - nesne mevcut değil." -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "Hatalı tip. Beklenen pk değeri, alınan %s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr "%s=%s objesi mevcut değil." -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "Geçersiz hiperlink - URL eşleşmesi yok" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "Geçersiz hiperlink - Doğru olmayan URL eşleşmesi" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "Yapılandırma hatasından dolayı geçersiz hiperlink" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "Geçersiz hiperlink - nesne mevcut değil." -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "Hatalı tip. Beklenen url dizges, alınan %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "Geçersiz veri" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "Girdi sağlanmadı" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "Yeni bir madde oluşturlamıyor, sadece var olanlar güncellenebilir." -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "Bir madde listesi bekleniyor." -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "Bulunamadı" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "İzin verilmedi" -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "Sunucu uygulaması hatası" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "Bağlantı hatası." -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "Bozulmuş talep." -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "Hatalı oturum açma bilgileri." -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "Oturum açma bilgileri girilmedi." -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "Bu eylemi gerçekleştirebilmek için gerekli izne sahip değilsiniz." -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "'%s' yöntemine izin verilmiyor." -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "" -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "'%s' talebinde desteklenmeyen ortam tipi mevcut" -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "" -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "" -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "Belirlenmeyen hata" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "Bulunamadı." -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "" -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "Hatalı parametreler." -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "Veri doğrulama hatası" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "Hatalı ya da geçersiz parametreler için Bütünlük Hatası " -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "Ön şart hatası" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "Daha fazla proje için yer kalmadı." -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "Parametre tipleri filtresinde hata." -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "'project' değeri numerik olmalı." -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "etiketler" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -423,7 +424,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -435,22 +436,6 @@ msgid "" " \n" " " msgstr "" -"\n" -"Taiga Destek:\n" -"%(support_url)s\n" -"
\n" -"Bize ulaşın:\n" -"\n" -"%(support_email)s\n" -"\n" -"
\n" -"E-posta listesi:\n" -"\n" -"%(mailing_list_url)s\n" -"" #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -501,103 +486,88 @@ msgstr "" "\n" "Yorumlar: %(comment)s" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "En azından bir role ihtiyacımız var" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "İhtiyaç duyulan döküm dosyası" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "Geçersiz döküm biçemi" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" bu projede bulunamadı" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "Geçersiz içerik. {\"key\": \"value\",...} şeklinde olması zorunlu" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "Geçersiz özel alanlar içeriyor." - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "Aynı isimde proje bulunmakta" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "İçeri aktarılan proje verisinde hata" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "İçeri aktarılan rollerde hata" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "İçeri aktarılan üyeliklerde hata" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "proje öznitelikleri listesi içeriye aktarılırken hata oluştu" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "varsayılan proje öznitelikleri değerlerinin içeriye aktarımında hata" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "özel öznitelikler içeri aktarılırken hata" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "İçeri aktarılan sprintlerde hata" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "İçeri aktarılan kullanıcı hikayelerinde hata" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "İçeri aktarılan görevlerde hata" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "İçeri aktarılan taleplerde hata" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "İçeri aktarılan kullanıcı hikayelerinde hata" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "İçeri aktarılan görevlerde hata" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "İçeri aktarılan wiki sayfalarında hata" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "İçeri aktarılan wiki bağlantılarında hata" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "İçeri aktarılan etiketlerde hata" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "zaman çizelgesi içeri aktarılırken hata" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "Proje dökümü oluşturulurken hata" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -617,15 +587,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "Proje dökümü yükleniyorken hata" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -861,77 +831,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] Projenizin döküm dosyası içe aktarıldı" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" bu projede bulunamadı" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Geçersiz içerik. {\"key\": \"value\",...} şeklinde olması zorunlu" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Geçersiz özel alanlar içeriyor." + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Aynı isimde proje bulunmakta" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "Kimlik doğrulama gerekli" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "isim" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "İkon url" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "web" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "tanı" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "Sonraki url" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "kullanıcı" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "uygulama" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "tam ad" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "e-posta adresi" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "yorum" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "oluşturma tarihi" @@ -960,7 +950,7 @@ msgstr "" "

%(comment)s

" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "Ekstra bilgi" @@ -994,513 +984,577 @@ msgstr "" "\n" "[Taiga] %(full_name)s <%(email)s> den geri bildirim\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "Proje mevcut değil." -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "Kötü imza" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: taiga/hooks/gitlab/event_hooks.py:74 -msgid "The referenced element doesn't exist" -msgstr "Referans gösterilmiş varlık mevcut değil" - -#: taiga/hooks/bitbucket/event_hooks.py:89 taiga/hooks/github/event_hooks.py:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "Durum mevcut değil" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "Bitbucket commiti ile durum değişti" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "Geçersiz talep bilgisi" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "Bitbucket ten oluşturulan talep" +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "Geçersiz talep yorum bilgisi" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Geçersiz talep bilgisi" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"Bitbucket yorum:\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "Githup commit i ile durum değişt." - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "GitHub dan oluşturulan talep" - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"GitHub dan gelen Yorum:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Referans gösterilmiş varlık mevcut değil" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Durum mevcut değil" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "GitLab dan oluşturuldu" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"Gitlabdan gelen yorum:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "Projeyi gör" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "Aşamaları gör" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "Kullanıcı hikayelerini gör" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "Görevleri gör" -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "Talepleri gör" -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "Wiki sayfalarını gör" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" msgstr "Wiki bağlantılarını gör" -#: taiga/permissions/permissions.py:39 -msgid "Request membership" -msgstr "Üyelik talep et" - -#: taiga/permissions/permissions.py:40 -msgid "Add user story to project" -msgstr "Projeye kullanıcı hikayesi ekle" - -#: taiga/permissions/permissions.py:41 -msgid "Add comments to user stories" -msgstr "Kullanıcı hikayelerine yorumlar ekle" - -#: taiga/permissions/permissions.py:42 -msgid "Add comments to tasks" -msgstr "Görevlere yorumlar ekle" - -#: taiga/permissions/permissions.py:43 -msgid "Add issues" -msgstr "Talepler ekle" - -#: taiga/permissions/permissions.py:44 -msgid "Add comments to issues" -msgstr "Taleplere yorumlar ekle" - -#: taiga/permissions/permissions.py:45 taiga/permissions/permissions.py:75 -msgid "Add wiki page" -msgstr "Wiki sayfası ekle" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "Wiki sayfası düzenle" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "Wiki bağlantısı ekle" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "Wiki bağlantısı düzenle" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "Aşama ekle" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "Aşama düzenle" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "Aşama sil" -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "Kullanıcı hikayesini gör" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "Kullanıcı hikayesi ekle" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "Kullanıcı hikayesi düzenle" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "Kullanıcı hikayesi sil" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "Görev ekle" -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "Görev düzenle" -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "Görev sil" -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "Talep ekle" -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "Talep düzenle" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "Talep sil" -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Wiki sayfası ekle" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Wiki sayfası düzenle" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "Wiki sayfası sil" -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Wiki bağlantısı ekle" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Wiki bağlantısı düzenle" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "Wiki bağlantısı sil" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "Proje düzenle" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "Üye ekle" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "Üye sil" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "Proje sil" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Üye ekle" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Üye sil" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "Admin proje değerleri" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "Yönetici rolleri" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "sahip" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "Eksik parametreq" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "Geçersiz resim biçemi" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "Geçersiz şablon adı" -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "Geçersiz şablon tanımı" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "Geçersiz kullanıcı id" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "Kullanıcı mevcut değil" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "Kullanıcı zaten proje üyesi durumunda" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "Görebilmek için yetkiniz yok." -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "Kısmi güncellemeler desteklenmiyor" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "proje" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "içerik tipi" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "nesne id" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "düzenleme tarihi" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "eklenmiş dosya" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "kaldırıldı" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "sıra" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "Özel" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "Yetkili kalmadığı için proje bloklandı" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "Metin" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "Çoklu-satır metin" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "Tarih" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "Url" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "tip" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "değerler" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "kullanıcı hikayesi" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "görev" -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "talep" -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "Aynı isimler bir tane daha mevcut." -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "durum" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "konu" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "renk" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "atanmış" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "istemci gereksinimi" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "takım gereksinimi" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "Yorum zaten silinmiş" -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "Yorum silinmedi" -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "Değiştir" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "Oluştur" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "Sil" @@ -1556,7 +1610,7 @@ msgstr "silindi" #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "Atanmamış" @@ -1603,95 +1657,75 @@ msgstr "Kimden:" msgid "To:" msgstr "Kime:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "içerik" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "engellenmiş not" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "sprint" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "Bu talep için bu sprinti ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "Bu talep için bu durumu ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "Bu talep için bu kritiklik derecesini ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "Bu talep için bu öncelik durumunu ayarlamaya yetkiniz yok." -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "Bu talep için bu tipi ayarlamaya yetkiniz yok." -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "durum" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "önem derecesi" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "öncelik" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "aşama" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "bitirme tarihi" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "konu" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "atanmış" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "dış referans" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "Beğen" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "Beğeniler" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "satır" @@ -1703,8 +1737,9 @@ msgstr "yaklaşık başlama tarihi" msgid "estimated finish date" msgstr "yaklaşık bitiş tarihi" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "kapatılmış" @@ -1716,120 +1751,132 @@ msgstr "taşınabilirlik" msgid "The estimated start must be previous to the estimated finish." msgstr "Tahmini başlangıç, tahmini bitişten önce olmalı" -#: taiga/projects/milestones/validators.py:12 -msgid "There's no sprint with that id" -msgstr "Bu id ye sahip sprint yok" +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "engellenmiş" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' parametresi zorunlu" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'proje' parametresi zorunlu" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "e-posta" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "kupon" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "Davetiye ekstra metni" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "kullanıcı sırası" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "Kullanıcı zaten projenin üyesi" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "varsayılan puanlar" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "varsayılan KH durumu" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "varsayılan puanlar" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "varsayılan görev durumu" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "varsayılan öncelik" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "varsayılan önem derecesi" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "varsayılan talep durumu" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "varsayılan talep tipi" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "üyeler" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "aşamaların toplamı" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "toplam hikaye puanı" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "aktif birikmiş iler paneli" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "aktif kanban paneli" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "aktif wiki paneli" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "aktif talep paneli" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "video konferans sistemi" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "videokonferans ekstra verisi" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "oluşturma şablonu" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "gizli" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "anonim izinler" @@ -1837,169 +1884,251 @@ msgstr "anonim izinler" msgid "user permissions" msgstr "kullanıcı izinleri" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "gizli" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr "vitrinde" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "insan arıyor" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "etiket renkleri" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "engellenmiş kod" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "yükleme tarih-saati" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "sayı" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "geçen hafta fanları" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "geçen ayın fanları" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "geçen yılın fanları" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "geçen haftanın aktiviteleri" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "geçen ayın aktiviteleri" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "geçen yılın aktiviteleri" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "modül ayarları" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "arşivlenmiş" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "renk" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "değer" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "varsayılan sahip rolü" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "varsayılan ayarlar" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "kh durumları" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "puanlar" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "görev durumları" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "talep durumları" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "talep tipleri" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "öncelikler" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "önem durumları" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "roller" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "Müdahil" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "Hepsi" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "Hiçbiri" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "oluşturma tarih-saati" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "tarihçe girdileri" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "kullanıcıları bilgilendir" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "İzlenen" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "Belirtilen kullanıcı ve proje için bilgilendirme mevcut" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "Bildirim düzeyi için geçersiz değer" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2537,141 +2666,135 @@ msgstr "" "\n" "[%(project)s] Silinmiş Wiki Sayfası \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "İzleyiciler arasında geçersiz kullanıcılar var" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "Sürüm rakamsal bir şey olmalıdır" -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "Sürüm parametresi geçersiz" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "Sürüm geçerli olanla uyuşmuyor" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "sürüm" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "E-posta adresi önceden alınmış" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "Proje için geçersiz rol" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "Varsayılan ayarlar" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "Kullanıcı hikayelerinin durumları" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "Puanlar" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "Görevlerin durumları" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "Taleplerin durumları" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "Taleplerin tipleri" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "Öncelikler" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "Önem dereceleri" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "Roller" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "Gelecek sprint" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "Proje Sonu" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "Kupon geçersiz" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "etiketler" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "etiket renkleri" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "Bu görev için sprint ayarlamanız için izniniz yok." -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "Bu görev için kullanıcı hikayesi ayarlama izniniz yok." -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "Bu görev için bu durumu ayarlama izniniz yok." @@ -2687,9 +2810,35 @@ msgstr "görev panosu sırası" msgid "is iocaine" msgstr "baldıran zehri" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "Bu id ile ilgili bir görev yok" +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3042,12 +3191,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3058,12 +3207,12 @@ msgid "" msgstr "" #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3072,303 +3221,388 @@ msgid "" msgstr "" #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "Yeni" #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "Hazır" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "Devam ediyor" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "Teste hazır" #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "Bitmiş" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "Arşivlenmiş" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "Kapatılmış" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "Bilgi İhtiyacı" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "Ertelenmiş" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "Reddedilmiş" #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "Hata" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "Soru" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "İyileştirme" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "Düşük" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "Normal" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "Yüksek" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "İstek Listesi" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "Önemli" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "Kritik" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "UX" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "Tasarım" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "Ön" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "Arka" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "Ürün Sahibi" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "Paydaş" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "Bu kullanıcı hikayesine bu sprinti ayarlama izniniz yok." -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "Bu kullanıcı hikayesine bu durumu ayarlama yetkiniz yok." -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "birikmiş işler sırası" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "sprint sırası" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "bitiş tarihi" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "istemci gereksinimi" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "takım gereksinimi" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "talepden oluştur" -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "Bu id ye sahip kullanıcı hikayesi yok" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "Bu id ye sahip proje yok" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "Bu id ye sahip kullanıcı hikayesi durumu yok" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "E-posta adresi önceden alınmış" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "Bu id ye sahip görev durumu yok" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Proje için geçersiz rol" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Varsayılan ayarlar" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Kullanıcı hikayelerinin durumları" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Puanlar" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Görevlerin durumları" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Taleplerin durumları" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Taleplerin tipleri" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Öncelikler" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Önem dereceleri" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Roller" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "Oylar" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "Oy" -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'content' parametresi zorunlu" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametresi zorunlu" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "son düzenleyen" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3396,148 +3630,140 @@ msgstr "" msgid "Important dates" msgstr "Önemli tarihler" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "Geçersiz e-posta" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "Geçersiz kullanıcı adı ya da e-posta" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "Posta başarıyla gönderildi!" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "Yeni parola parametresi gerekli" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "Geçersiz parola uzunluğu, en az 6 karaktere ihtiyaç var" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "" "Geçersiz geçerli bir kupona sahip olduğunuzdan ve bu kuponu daha önce " "kullanmadığınızdan emin misiniz?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "Geçersiz, kuponun doğru olduğuna emin misin?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "superuser durumu" -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "" -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "kullanıcı adı" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "" "Zorunlu. 30 karakter ya da daha azı. Harfler, sayılar ve /./-/_ karakterleri" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "Geçerli bir kullanıcı adı girin." -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "aktif" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "" -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "biyografi" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "fotoğraf" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "katılma tarihi" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "varsayılan dil" -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "varsayılan tema" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "varsayılan saat dilimi" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "etiketleri renklendir" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "e-posta kuponu" -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "yeni e-posta adresi" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "izinler" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "Geçersiz" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin." - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "Kullanıcı adı veya parola kullanıcıyla uyuşmuyor" @@ -3660,47 +3886,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "Taigalandınız!" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "Bu id ye sahip yok yok" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "Geçersiz" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin." + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "" -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "anahtar" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "gizli anahtar" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "durum kodu" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "talep verisi" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "talep başlıkları" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "cevap verisi" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "cevap başlıkları" -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "süre" diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po index 5537e07f..9a658433 100644 --- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po +++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po @@ -11,8 +11,8 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-01 19:09+0200\n" -"PO-Revision-Date: 2016-05-01 17:09+0000\n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/" "taiga-back/language/zh-Hant/)\n" @@ -22,339 +22,340 @@ msgstr "" "Language: zh-Hant\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: taiga/auth/api.py:100 +#: taiga/auth/api.py:102 msgid "Public register is disabled." msgstr "註冊功能暫不開放" -#: taiga/auth/api.py:133 +#: taiga/auth/api.py:135 msgid "invalid register type" msgstr "無效的註冊類型" -#: taiga/auth/api.py:146 +#: taiga/auth/api.py:148 msgid "invalid login type" msgstr "無效的登入類型" -#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64 +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "本用戶名稱已被註冊" + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "本電子郵件已使用" + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "代碼與任何有效的邀請不相符" + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "使用者已被註冊。" + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "使用者已是專案成員" + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "無法創建新使用者" + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "無效的代碼 " + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 msgid "invalid username" msgstr "無效使用者名稱" -#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70 +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 msgid "" "Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" msgstr "必填。最多255字元(可為數字,字母,符號....)" -#: 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:94 -msgid "Token not matches any valid invitation." -msgstr "代碼與任何有效的邀請不相符" - -#: taiga/auth/services.py:122 -msgid "User is already registered." -msgstr "使用者已被註冊。" - -#: taiga/auth/services.py:146 -msgid "This user is already a member of the project." -msgstr "使用者已是專案成員" - -#: taiga/auth/services.py:172 -msgid "Error on creating new user." -msgstr "無法創建新使用者" - -#: taiga/auth/tokens.py:48 taiga/auth/tokens.py:55 -#: taiga/external_apps/services.py:35 taiga/projects/api.py:376 -#: taiga/projects/api.py:397 -msgid "Invalid token" -msgstr "無效的代碼 " - -#: taiga/base/api/fields.py:292 +#: taiga/base/api/fields.py:294 msgid "This field is required." msgstr "此欄位是必要的。" -#: taiga/base/api/fields.py:293 taiga/base/api/relations.py:335 +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 msgid "Invalid value." msgstr "無效的數值" -#: taiga/base/api/fields.py:477 +#: taiga/base/api/fields.py:479 #, python-format msgid "'%s' value must be either True or False." msgstr "'%s' 數值必須為「是」或「否」。" -#: taiga/base/api/fields.py:541 +#: taiga/base/api/fields.py:543 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "輸入有效的代稱,其包括字母,數字,底底線與連字符號" -#: taiga/base/api/fields.py:556 +#: taiga/base/api/fields.py:558 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "請做個有效的選擇。 %(value)s 並不是可以選的選項。" -#: taiga/base/api/fields.py:619 +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 msgid "Enter a valid email address." msgstr "輸入無效之電子郵件地址" -#: taiga/base/api/fields.py:661 +#: taiga/base/api/fields.py:672 #, python-format msgid "Date has wrong format. Use one of these formats instead: %s" msgstr "資料格式錯誤,請改用這些格式取代:%s" -#: taiga/base/api/fields.py:725 +#: taiga/base/api/fields.py:736 #, python-format msgid "Datetime has wrong format. Use one of these formats instead: %s" msgstr "日期格式錯誤,請使用這些格式取代:%s" -#: taiga/base/api/fields.py:795 +#: taiga/base/api/fields.py:806 #, python-format msgid "Time has wrong format. Use one of these formats instead: %s" msgstr "時間格式錯誤,請使用這些格式取代:%s" -#: taiga/base/api/fields.py:852 +#: taiga/base/api/fields.py:863 msgid "Enter a whole number." msgstr "輸入一個整數" -#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906 +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "確認此值小於等於 %(limit_value)s." -#: taiga/base/api/fields.py:854 taiga/base/api/fields.py:907 +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "確認此值大於等於 %(limit_value)s." -#: taiga/base/api/fields.py:884 +#: taiga/base/api/fields.py:895 #, python-format msgid "\"%s\" value must be a float." msgstr "\"%s\" 數值必須為一個浮點數" -#: taiga/base/api/fields.py:905 +#: taiga/base/api/fields.py:916 msgid "Enter a number." msgstr "輸入一組號碼" -#: taiga/base/api/fields.py:908 +#: taiga/base/api/fields.py:919 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "確認全部沒有多於 %s位數 " -#: taiga/base/api/fields.py:909 +#: taiga/base/api/fields.py:920 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "確認沒有多於 %s十進位數 " -#: taiga/base/api/fields.py:910 +#: taiga/base/api/fields.py:921 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "確認在小數點前沒有多於 %s位數 " -#: taiga/base/api/fields.py:977 +#: taiga/base/api/fields.py:988 msgid "No file was submitted. Check the encoding type on the form." msgstr "無檔案送出,請 確認表格中的編碼 格式" -#: taiga/base/api/fields.py:978 +#: taiga/base/api/fields.py:989 msgid "No file was submitted." msgstr "無檔案送出" -#: taiga/base/api/fields.py:979 +#: taiga/base/api/fields.py:990 msgid "The submitted file is empty." msgstr "送出的檔案無內容" -#: taiga/base/api/fields.py:980 +#: taiga/base/api/fields.py:991 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr "確認檔案名稱最多有 %(max)d 字元 (它有 %(length)d)." -#: taiga/base/api/fields.py:981 +#: taiga/base/api/fields.py:992 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "請上傳擋案或是勾選清除方格中二選一" -#: taiga/base/api/fields.py:1021 +#: taiga/base/api/fields.py:1032 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞" -#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209 -#: taiga/hooks/api.py:68 taiga/projects/api.py:642 -#: taiga/projects/issues/api.py:233 taiga/projects/mixins/ordering.py:58 -#: taiga/projects/tasks/api.py:152 taiga/projects/tasks/api.py:174 -#: taiga/projects/userstories/api.py:218 taiga/projects/userstories/api.py:238 -#: taiga/webhooks/api.py:68 +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" -#: taiga/base/api/pagination.py:213 +#: taiga/base/api/pagination.py:214 msgid "Page is not 'last', nor can it be converted to an int." msgstr "頁數不是最後,或者它無法轉成整數 " -#: taiga/base/api/pagination.py:217 +#: taiga/base/api/pagination.py:218 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" msgstr "無效頁面I (%(page_number)s): %(message)s" -#: taiga/base/api/permissions.py:64 +#: taiga/base/api/permissions.py:66 msgid "Invalid permission definition." msgstr "無效的權限定義 " -#: taiga/base/api/relations.py:245 +#: taiga/base/api/relations.py:247 #, python-format msgid "Invalid pk '%s' - object does not exist." msgstr "無效的pk '%s'- 物件並不存在" -#: taiga/base/api/relations.py:246 +#: taiga/base/api/relations.py:248 #, python-format msgid "Incorrect type. Expected pk value, received %s." msgstr "不正確類型,預期為pk值,收到%s." -#: taiga/base/api/relations.py:334 +#: taiga/base/api/relations.py:336 #, python-format msgid "Object with %s=%s does not exist." msgstr " 包含%s=%s物件不存在" -#: taiga/base/api/relations.py:370 +#: taiga/base/api/relations.py:372 msgid "Invalid hyperlink - No URL match" msgstr "無效的超鏈接 - 無相符之網址" -#: taiga/base/api/relations.py:371 +#: taiga/base/api/relations.py:373 msgid "Invalid hyperlink - Incorrect URL match" msgstr "無效的超鏈接 - 不正確的相符網址" -#: taiga/base/api/relations.py:372 +#: taiga/base/api/relations.py:374 msgid "Invalid hyperlink due to configuration error" msgstr "因設定出錯的無效超鏈接" -#: taiga/base/api/relations.py:373 +#: taiga/base/api/relations.py:375 msgid "Invalid hyperlink - object does not exist." msgstr "無效的超鏈接 - 物件並不存在" -#: taiga/base/api/relations.py:374 +#: taiga/base/api/relations.py:376 #, python-format msgid "Incorrect type. Expected url string, received %s." msgstr "不正確類型,預期為網址格式,收到的是 %s." -#: taiga/base/api/serializers.py:320 +#: taiga/base/api/serializers.py:324 msgid "Invalid data" msgstr "無效的資料" -#: taiga/base/api/serializers.py:412 +#: taiga/base/api/serializers.py:416 msgid "No input provided" msgstr "無輸入提供" -#: taiga/base/api/serializers.py:575 +#: taiga/base/api/serializers.py:579 msgid "Cannot create a new item, only existing items may be updated." msgstr "無法建立新項目,只能更新現有項目" -#: taiga/base/api/serializers.py:586 +#: taiga/base/api/serializers.py:590 msgid "Expected a list of items." msgstr "期待的項目清單" -#: taiga/base/api/views.py:125 +#: taiga/base/api/views.py:126 msgid "Not found" msgstr "找不到" -#: taiga/base/api/views.py:128 +#: taiga/base/api/views.py:129 msgid "Permission denied" msgstr "許可遭拒絕 " -#: taiga/base/api/views.py:476 +#: taiga/base/api/views.py:477 msgid "Server application error" msgstr "伺服器應用出錯" -#: taiga/base/connectors/exceptions.py:25 +#: taiga/base/connectors/exceptions.py:26 msgid "Connection error." msgstr "連結出錯" -#: taiga/base/exceptions.py:77 +#: taiga/base/exceptions.py:79 msgid "Malformed request." msgstr "遭封鎖" -#: taiga/base/exceptions.py:82 +#: taiga/base/exceptions.py:84 msgid "Incorrect authentication credentials." msgstr "不正確的授權認證 " -#: taiga/base/exceptions.py:87 +#: taiga/base/exceptions.py:89 msgid "Authentication credentials were not provided." msgstr "未担供授權認證 " -#: taiga/base/exceptions.py:92 +#: taiga/base/exceptions.py:94 msgid "You do not have permission to perform this action." msgstr "你無權限進行此動作" -#: taiga/base/exceptions.py:97 +#: taiga/base/exceptions.py:99 #, python-format msgid "Method '%s' not allowed." msgstr "不允許 '%s' 方式" -#: taiga/base/exceptions.py:105 +#: taiga/base/exceptions.py:107 msgid "Could not satisfy the request's Accept header" msgstr "無法滿藙要求其接受標頭 " -#: taiga/base/exceptions.py:114 +#: taiga/base/exceptions.py:116 #, python-format msgid "Unsupported media type '%s' in request." msgstr "不支援的資料類型'%s' 被提出" -#: taiga/base/exceptions.py:122 +#: taiga/base/exceptions.py:124 msgid "Request was throttled." msgstr "要求無法執行 " -#: taiga/base/exceptions.py:123 +#: taiga/base/exceptions.py:125 #, python-format msgid "Expected available in %d second%s." msgstr "預期在 %d 秒%s.內可取得 " -#: taiga/base/exceptions.py:137 +#: taiga/base/exceptions.py:139 msgid "Unexpected error" msgstr "無預期的錯誤" -#: taiga/base/exceptions.py:149 +#: taiga/base/exceptions.py:151 msgid "Not found." msgstr "找不到" -#: taiga/base/exceptions.py:154 +#: taiga/base/exceptions.py:156 msgid "Method not supported for this endpoint." msgstr "從GitHub取得原始碼" -#: taiga/base/exceptions.py:162 taiga/base/exceptions.py:170 +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 msgid "Wrong arguments." msgstr "錯誤的參數" -#: taiga/base/exceptions.py:174 +#: taiga/base/exceptions.py:176 msgid "Data validation error" msgstr "資料有效性錯誤" -#: taiga/base/exceptions.py:186 +#: taiga/base/exceptions.py:188 msgid "Integrity Error for wrong or invalid arguments" msgstr "因錯誤或無效參數,一致性出錯" -#: taiga/base/exceptions.py:193 +#: taiga/base/exceptions.py:195 msgid "Precondition error" msgstr "前提出錯" -#: taiga/base/exceptions.py:217 +#: taiga/base/exceptions.py:219 msgid "No room left for more projects." msgstr "" -#: taiga/base/filters.py:79 taiga/base/filters.py:444 +#: taiga/base/filters.py:81 taiga/base/filters.py:462 msgid "Error in filter params types." msgstr "過濾參數類型出錯" -#: taiga/base/filters.py:133 taiga/base/filters.py:232 -#: taiga/projects/filters.py:63 +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 msgid "'project' must be an integer value." msgstr "專案須為整數值" -#: taiga/base/tags.py:26 -msgid "tags" -msgstr "標籤" - #: taiga/base/templates/emails/base-body-html.jinja:6 msgid "Taiga" msgstr "Taiga" @@ -409,7 +410,7 @@ msgid "" " Contact us:\n" " \n" +"%(support_email)s\" title=\"Support email\" style=\"color: #9dce0a\">\n" " %(support_email)s\n" " \n" "
\n" @@ -421,33 +422,6 @@ msgid "" " \n" " " msgstr "" -"\n" -"Taiga 支援:\n" -"\n" -"" -"%(support_url)s\n" -"\n" -"
\n" -"\n" -"聯絡我們:\n" -"\n" -"\n" -"\n" -"%(support_email)s\n" -"\n" -"\n" -"\n" -"
\n" -"\n" -"郵件群組:\n" -"\n" -"\n" -"\n" -"%(mailing_list_url)s\n" -"\n" -"" #: taiga/base/templates/emails/hero-body-html.jinja:6 msgid "You have been Taigatized" @@ -498,103 +472,88 @@ msgstr "" "\n" "評論: %(comment)s" -#: taiga/export_import/api.py:119 +#: taiga/export_import/api.py:127 msgid "We needed at least one role" msgstr "我們至少需要一個角色" -#: taiga/export_import/api.py:309 +#: taiga/export_import/api.py:323 msgid "Needed dump file" msgstr "需要的堆存檔案" -#: taiga/export_import/api.py:316 +#: taiga/export_import/api.py:333 msgid "Invalid dump format" msgstr "無效堆存格式" -#: taiga/export_import/serializers.py:178 -msgid "{}=\"{}\" not found in this project" -msgstr "{}=\"{}\" 無法在此專案中找到" - -#: taiga/export_import/serializers.py:443 -#: taiga/projects/custom_attributes/serializers.py:104 -msgid "Invalid content. It must be {\"key\": \"value\",...}" -msgstr "無效內容。必須為 {\"key\": \"value\",...}" - -#: taiga/export_import/serializers.py:458 -#: taiga/projects/custom_attributes/serializers.py:119 -msgid "It contain invalid custom fields." -msgstr "包括無效慣例欄位" - -#: taiga/export_import/serializers.py:528 -#: taiga/projects/mixins/serializers.py:38 -msgid "Name duplicated for the project" -msgstr "專案的名稱被複製了" - -#: taiga/export_import/services/store.py:621 -#: taiga/export_import/services/store.py:639 +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 msgid "error importing project data" msgstr "滙入重要專案資料出錯" -#: taiga/export_import/services/store.py:646 +#: taiga/export_import/services/store.py:743 msgid "error importing roles" msgstr "滙入角色出錯" -#: taiga/export_import/services/store.py:651 +#: taiga/export_import/services/store.py:748 msgid "error importing memberships" msgstr "滙入成員資格出錯" -#: taiga/export_import/services/store.py:661 +#: taiga/export_import/services/store.py:759 msgid "error importing lists of project attributes" msgstr "滙入標籤出錯" -#: taiga/export_import/services/store.py:665 +#: taiga/export_import/services/store.py:763 msgid "error importing default project attributes values" msgstr "滙入預設專案屬性數值出錯" -#: taiga/export_import/services/store.py:674 +#: taiga/export_import/services/store.py:774 msgid "error importing custom attributes" msgstr "滙入客制性屬出錯" -#: taiga/export_import/services/store.py:679 +#: taiga/export_import/services/store.py:778 msgid "error importing sprints" msgstr "滙入衝刺任務出錯" -#: taiga/export_import/services/store.py:683 -msgid "error importing user stories" -msgstr "滙入使用者故事出錯" - -#: taiga/export_import/services/store.py:687 -msgid "error importing tasks" -msgstr "滙入任務出錯" - -#: taiga/export_import/services/store.py:691 +#: taiga/export_import/services/store.py:782 msgid "error importing issues" msgstr "滙入問題出錯" -#: taiga/export_import/services/store.py:695 +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "滙入使用者故事出錯" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "滙入任務出錯" + +#: taiga/export_import/services/store.py:798 msgid "error importing wiki pages" msgstr "滙入維基頁出錯" -#: taiga/export_import/services/store.py:699 +#: taiga/export_import/services/store.py:802 msgid "error importing wiki links" msgstr "滙入維基連結出錯" -#: taiga/export_import/services/store.py:703 +#: taiga/export_import/services/store.py:806 msgid "error importing tags" msgstr "滙入標籤出錯" -#: taiga/export_import/services/store.py:707 +#: taiga/export_import/services/store.py:810 msgid "error importing timelines" msgstr "滙入時間軸出錯" -#: taiga/export_import/services/store.py:731 +#: taiga/export_import/services/store.py:832 msgid "unexpected error importing project" msgstr "" -#: taiga/export_import/tasks.py:56 taiga/export_import/tasks.py:57 +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 msgid "Error generating project dump" msgstr "產生專案傾倒時出錯" -#: taiga/export_import/tasks.py:81 +#: taiga/export_import/tasks.py:91 #, python-brace-format msgid "" "\n" @@ -614,15 +573,15 @@ msgid "" "------------" msgstr "" -#: taiga/export_import/tasks.py:110 +#: taiga/export_import/tasks.py:120 msgid "Error loading project dump" msgstr "載入專案傾倒時出錯" -#: taiga/export_import/tasks.py:111 +#: taiga/export_import/tasks.py:121 msgid "Error loading your project dump file" msgstr "" -#: taiga/export_import/tasks.py:125 +#: taiga/export_import/tasks.py:135 msgid " -- no detail info --" msgstr "" @@ -858,77 +817,97 @@ msgstr "" msgid "[%(project)s] Your project dump has been imported" msgstr "[%(project)s] 您堆存的專案已滙入" -#: taiga/external_apps/api.py:41 taiga/external_apps/api.py:67 -#: taiga/external_apps/api.py:74 +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" 無法在此專案中找到" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "無效內容。必須為 {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "包括無效慣例欄位" + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "專案的名稱被複製了" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 msgid "Authentication required" msgstr "要求取得授權" -#: taiga/external_apps/models.py:34 +#: taiga/external_apps/models.py:35 #: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146 -#: taiga/projects/models.py:478 taiga/projects/models.py:517 -#: taiga/projects/models.py:542 taiga/projects/models.py:579 -#: taiga/projects/models.py:602 taiga/projects/models.py:625 -#: taiga/projects/models.py:660 taiga/projects/models.py:683 -#: taiga/users/admin.py:53 taiga/users/models.py:292 -#: taiga/webhooks/models.py:28 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "姓名" -#: taiga/external_apps/models.py:36 +#: taiga/external_apps/models.py:37 msgid "Icon url" msgstr "網址圖標" -#: taiga/external_apps/models.py:37 +#: taiga/external_apps/models.py:38 msgid "web" msgstr "網頁" -#: taiga/external_apps/models.py:38 taiga/projects/attachments/models.py:60 +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 #: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/history/templatetags/functions.py:24 -#: taiga/projects/issues/models.py:62 taiga/projects/models.py:150 -#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:92 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 msgid "description" msgstr "描述" -#: taiga/external_apps/models.py:40 +#: taiga/external_apps/models.py:41 msgid "Next url" msgstr "下一個網址" -#: taiga/external_apps/models.py:42 +#: taiga/external_apps/models.py:43 msgid "secret key for ciphering the application tokens" msgstr "應用程式的密碼字符數列" -#: taiga/external_apps/models.py:56 taiga/projects/likes/models.py:30 -#: taiga/projects/notifications/models.py:86 taiga/projects/votes/models.py:51 +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 msgid "user" msgstr "使用者" -#: taiga/external_apps/models.py:60 +#: taiga/external_apps/models.py:61 msgid "application" msgstr "應用程式" -#: taiga/feedback/models.py:24 taiga/users/models.py:138 +#: taiga/feedback/models.py:25 taiga/users/models.py:137 msgid "full name" msgstr "全名" -#: taiga/feedback/models.py:26 taiga/users/models.py:133 +#: taiga/feedback/models.py:27 taiga/users/models.py:132 msgid "email address" msgstr "電子郵件" -#: taiga/feedback/models.py:28 +#: taiga/feedback/models.py:29 msgid "comment" msgstr "評論" -#: taiga/feedback/models.py:30 taiga/projects/attachments/models.py:47 +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/issues/models.py:54 taiga/projects/likes/models.py:32 -#: taiga/projects/milestones/models.py:49 taiga/projects/models.py:157 -#: taiga/projects/models.py:689 taiga/projects/notifications/models.py:88 -#: taiga/projects/tasks/models.py:47 taiga/projects/userstories/models.py:84 -#: taiga/projects/votes/models.py:53 taiga/projects/wiki/models.py:40 -#: taiga/userstorage/models.py:28 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 msgid "created date" msgstr "創建日期" @@ -957,7 +936,7 @@ msgstr "" "

%(comment)s

" #: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 -#: taiga/users/admin.py:120 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 msgid "Extra info" msgstr "額外資訊" @@ -990,543 +969,577 @@ msgstr "" "\n" "[Taiga] 回饋來自 %(full_name)s <%(email)s>\n" -#: taiga/hooks/api.py:53 +#: taiga/hooks/api.py:54 msgid "The payload is not a valid json" msgstr "載荷為無效json" -#: taiga/hooks/api.py:62 taiga/projects/issues/api.py:139 -#: taiga/projects/tasks/api.py:86 taiga/projects/userstories/api.py:111 +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 msgid "The project doesn't exist" msgstr "專案不存在" -#: taiga/hooks/api.py:65 +#: taiga/hooks/api.py:66 msgid "Bad signature" msgstr "錯誤簽名" -#: taiga/hooks/bitbucket/event_hooks.py:82 taiga/hooks/github/event_hooks.py:76 -#: 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:83 -#: taiga/hooks/gitlab/event_hooks.py:81 -msgid "The status doesn't exist" -msgstr "狀態不存在" - -#: taiga/hooks/bitbucket/event_hooks.py:95 -msgid "Status changed from BitBucket commit" -msgstr "來自BitBucket 投入的狀態更新" - -#: taiga/hooks/bitbucket/event_hooks.py:124 -#: taiga/hooks/github/event_hooks.py:142 taiga/hooks/gitlab/event_hooks.py:114 -msgid "Invalid issue information" -msgstr "無效的問題資訊" - -#: taiga/hooks/bitbucket/event_hooks.py:140 +#: taiga/hooks/event_hooks.py:66 #, python-brace-format msgid "" -"Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" "\n" -"{description}" +"\"{comment_message}\"" msgstr "" -"來自[@{bitbucket_user_name}]({bitbucket_user_url} 的問題\"詳見 " -"@{bitbucket_user_name}'s BitBucket profile\") BitBucket.\n" -"源自BitBucket 問題: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\"):\n" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" "\n" -"{description}" +"> {comment_message}" +msgstr "" -#: taiga/hooks/bitbucket/event_hooks.py:151 -msgid "Issue created from BitBucket." -msgstr "來自BitBucket的問題:" - -#: taiga/hooks/bitbucket/event_hooks.py:175 -#: taiga/hooks/github/event_hooks.py:178 taiga/hooks/github/event_hooks.py:193 -#: taiga/hooks/gitlab/event_hooks.py:153 +#: taiga/hooks/event_hooks.py:84 msgid "Invalid issue comment information" msgstr "無效的議題評論資訊" -#: taiga/hooks/bitbucket/event_hooks.py:183 +#: taiga/hooks/event_hooks.py:103 #, python-brace-format msgid "" -"Comment by [@{bitbucket_user_name}]({bitbucket_user_url} \"See " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"Origin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." msgstr "" -" [@{bitbucket_user_name}]({bitbucket_user_url}之評論 \"參見 " -"@{bitbucket_user_name}'s BitBucket profile\") from BitBucket.\n" -"源自BitBucket 問題: [bb#{number} - {subject}]({bitbucket_url} \"Go to " -"'bb#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/bitbucket/event_hooks.py:194 +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "無效的問題資訊" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 #, python-brace-format msgid "" -"Comment From BitBucket:\n" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" "\n" -"{message}" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"來自BitBucket的評論:\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:97 +#: taiga/hooks/event_hooks.py:161 #, python-brace-format msgid "" -"Status changed by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub commit [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" msgstr "" -"來自 [@{github_user_name}]({github_user_url}之狀態變更 \"參見" -"@{github_user_name}'s GitHub profile\") 來自GitHub之投入 [{commit_id}]" -"({commit_url} \"See commit '{commit_id} - {commit_message}'\")." -#: taiga/hooks/github/event_hooks.py:108 -msgid "Status changed from GitHub commit." -msgstr "來自GitHub投入的狀態更新" - -#: taiga/hooks/github/event_hooks.py:158 +#: taiga/hooks/event_hooks.py:179 #, python-brace-format msgid "" -"Issue created by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\"):\n" -"\n" -"{description}" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" msgstr "" -"來自 [@{github_user_name}]({github_user_url}提出的問題 \"參見" -"@{github_user_name}'s GitHub profile\") 來自GitHub. Github上原始問題 : " -"[gh#{number} - {subject}]({github_url} ”跳至 'gh#{number} - {subject}'\") \n" -"{description}" -#: taiga/hooks/github/event_hooks.py:169 -msgid "Issue created from GitHub." -msgstr "自來GitHub 的問題 " - -#: taiga/hooks/github/event_hooks.py:201 +#: taiga/hooks/event_hooks.py:184 #, python-brace-format msgid "" -"Comment by [@{github_user_name}]({github_user_url} \"See " -"@{github_user_name}'s GitHub profile\") from GitHub.\n" -"Origin GitHub issue: [gh#{number} - {subject}]({github_url} \"Go to " -"'gh#{number} - {subject}'\")\n" -"\n" -"{message}" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" msgstr "" -"來自 [@{github_user_name}]({github_user_url}之評論 \"參見" -"@{github_user_name}'s GitHub profile\") 來自GitHub. Gibhub上原始問題 : " -"[gh#{number} - {subject}]({github_url} ”跳至 'gh#{number} - {subject}'\")\n" -"\n" -"{message}" -#: taiga/hooks/github/event_hooks.py:212 -#, python-brace-format -msgid "" -"Comment From GitHub:\n" -"\n" -"{message}" -msgstr "" -"來自 GitHub:\n" -"\n" -"{message}" +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "參考元素不存在" -#: taiga/hooks/gitlab/event_hooks.py:87 -msgid "Status changed from GitLab commit" -msgstr "來自GitLab提供的狀態變更" +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "狀態不存在" -#: taiga/hooks/gitlab/event_hooks.py:129 -msgid "Created from GitLab" -msgstr "創建立GitLab" - -#: taiga/hooks/gitlab/event_hooks.py:161 -#, python-brace-format -msgid "" -"Comment by [@{gitlab_user_name}]({gitlab_user_url} \"See " -"@{gitlab_user_name}'s GitLab profile\") from GitLab.\n" -"Origin GitLab issue: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" -msgstr "" -" [@{gitlab_user_name}]({gitlab_user_url}之評論 \"參見 @{gitlab_user_name}'s " -"GitLab profile\") from GitLab.\n" -"源自 GitLab 問題: [gl#{number} - {subject}]({gitlab_url} \"Go to " -"'gl#{number} - {subject}'\")\n" -"\n" -"{message}" - -#: taiga/hooks/gitlab/event_hooks.py:172 -#, python-brace-format -msgid "" -"Comment From GitLab:\n" -"\n" -"{message}" -msgstr "" -"來自GitLab的評論:\n" -"\n" -"{message}" - -#: taiga/permissions/permissions.py:22 taiga/permissions/permissions.py:32 -#: taiga/permissions/permissions.py:52 +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 msgid "View project" msgstr "檢視專案" -#: taiga/permissions/permissions.py:23 taiga/permissions/permissions.py:33 -#: taiga/permissions/permissions.py:54 +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 msgid "View milestones" msgstr "檢視里程碑" -#: taiga/permissions/permissions.py:24 taiga/permissions/permissions.py:34 +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 msgid "View user stories" msgstr "檢視使用者故事" -#: taiga/permissions/permissions.py:25 taiga/permissions/permissions.py:36 -#: taiga/permissions/permissions.py:64 +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 msgid "View tasks" msgstr "檢視任務 " -#: taiga/permissions/permissions.py:26 taiga/permissions/permissions.py:35 -#: taiga/permissions/permissions.py:69 +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 msgid "View issues" msgstr "檢視問題 " -#: taiga/permissions/permissions.py:27 taiga/permissions/permissions.py:37 -#: taiga/permissions/permissions.py:74 +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 msgid "View wiki pages" msgstr "檢視維基頁" -#: taiga/permissions/permissions.py:28 taiga/permissions/permissions.py:38 -#: taiga/permissions/permissions.py:79 +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 msgid "View wiki links" 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:75 -msgid "Add wiki page" -msgstr "新增維基頁" - -#: taiga/permissions/permissions.py:46 taiga/permissions/permissions.py:76 -msgid "Modify wiki page" -msgstr "修改維基頁" - -#: taiga/permissions/permissions.py:47 taiga/permissions/permissions.py:80 -msgid "Add wiki link" -msgstr "新增維基連結" - -#: taiga/permissions/permissions.py:48 taiga/permissions/permissions.py:81 -msgid "Modify wiki link" -msgstr "修改維基連結" - -#: taiga/permissions/permissions.py:55 +#: taiga/permissions/choices.py:37 msgid "Add milestone" msgstr "加入里程碑" -#: taiga/permissions/permissions.py:56 +#: taiga/permissions/choices.py:38 msgid "Modify milestone" msgstr "修改里程碑" -#: taiga/permissions/permissions.py:57 +#: taiga/permissions/choices.py:39 msgid "Delete milestone" msgstr "刪除里程碑 " -#: taiga/permissions/permissions.py:59 +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 msgid "View user story" msgstr "檢視使用者故事" -#: taiga/permissions/permissions.py:60 +#: taiga/permissions/choices.py:48 msgid "Add user story" msgstr "新增使用者故事" -#: taiga/permissions/permissions.py:61 +#: taiga/permissions/choices.py:49 msgid "Modify user story" msgstr "修改使用者故事" -#: taiga/permissions/permissions.py:62 +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 msgid "Delete user story" msgstr "刪除使用者故事" -#: taiga/permissions/permissions.py:65 +#: taiga/permissions/choices.py:54 msgid "Add task" msgstr "新增任務 " -#: taiga/permissions/permissions.py:66 +#: taiga/permissions/choices.py:55 msgid "Modify task" msgstr "修改任務 " -#: taiga/permissions/permissions.py:67 +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 msgid "Delete task" msgstr "刪除任務 " -#: taiga/permissions/permissions.py:70 +#: taiga/permissions/choices.py:60 msgid "Add issue" msgstr "新增問題 " -#: taiga/permissions/permissions.py:71 +#: taiga/permissions/choices.py:61 msgid "Modify issue" msgstr "修改問題" -#: taiga/permissions/permissions.py:72 +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 msgid "Delete issue" msgstr "刪除問題 " -#: taiga/permissions/permissions.py:77 +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "新增維基頁" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "修改維基頁" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 msgid "Delete wiki page" msgstr "刪除維基頁 " -#: taiga/permissions/permissions.py:82 +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "新增維基連結" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "修改維基連結" + +#: taiga/permissions/choices.py:74 msgid "Delete wiki link" msgstr "刪除維基連結" -#: taiga/permissions/permissions.py:86 +#: taiga/permissions/choices.py:78 msgid "Modify project" msgstr "修改專案" -#: taiga/permissions/permissions.py:87 -msgid "Add member" -msgstr "新增成員" - -#: taiga/permissions/permissions.py:88 -msgid "Remove member" -msgstr "移除成員" - -#: taiga/permissions/permissions.py:89 +#: taiga/permissions/choices.py:79 msgid "Delete project" msgstr "刪除專案" -#: taiga/permissions/permissions.py:90 +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "新增成員" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "移除成員" + +#: taiga/permissions/choices.py:82 msgid "Admin project values" msgstr "管理員專案數值" -#: taiga/permissions/permissions.py:91 +#: taiga/permissions/choices.py:83 msgid "Admin roles" msgstr "管理員角色" -#: taiga/projects/admin.py:90 taiga/projects/attachments/models.py:38 -#: taiga/projects/issues/models.py:39 taiga/projects/milestones/models.py:43 -#: taiga/projects/models.py:162 taiga/projects/notifications/models.py:61 -#: taiga/projects/tasks/models.py:38 taiga/projects/userstories/models.py:66 -#: taiga/projects/wiki/models.py:36 taiga/users/admin.py:69 -#: taiga/userstorage/models.py:26 +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "所有者" -#: taiga/projects/api.py:165 taiga/users/api.py:220 +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 msgid "Incomplete arguments" msgstr "不完整參數" -#: taiga/projects/api.py:169 taiga/users/api.py:225 +#: taiga/projects/api.py:154 taiga/users/api.py:242 msgid "Invalid image format" msgstr "無效的圖片檔案" -#: taiga/projects/api.py:230 +#: taiga/projects/api.py:215 msgid "Not valid template name" msgstr "非有效樣板名稱 " -#: taiga/projects/api.py:233 +#: taiga/projects/api.py:218 msgid "Not valid template description" msgstr "無效樣板描述" -#: taiga/projects/api.py:356 +#: taiga/projects/api.py:344 msgid "Invalid user id" msgstr "" -#: taiga/projects/api.py:362 +#: taiga/projects/api.py:350 msgid "The user doesn't exist" msgstr "" -#: taiga/projects/api.py:366 +#: taiga/projects/api.py:354 msgid "The user must be already a project member" msgstr "" -#: taiga/projects/api.py:672 +#: taiga/projects/api.py:701 msgid "" "The project must have an owner and at least one of the users must be an " "active admin" msgstr "" -#: taiga/projects/api.py:706 +#: taiga/projects/api.py:735 msgid "You don't have permisions to see that." msgstr "您無觀看權限" -#: taiga/projects/attachments/api.py:51 +#: taiga/projects/attachments/api.py:54 msgid "Partial updates are not supported" msgstr "不支援部份更新" -#: taiga/projects/attachments/api.py:66 +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 msgid "Project ID not matches between object and project" msgstr "專案ID不符合物件與專案" -#: taiga/projects/attachments/models.py:40 +#: taiga/projects/attachments/models.py:41 #: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/issues/models.py:52 taiga/projects/milestones/models.py:45 -#: taiga/projects/models.py:466 taiga/projects/models.py:492 -#: taiga/projects/models.py:523 taiga/projects/models.py:552 -#: taiga/projects/models.py:585 taiga/projects/models.py:608 -#: taiga/projects/models.py:635 taiga/projects/models.py:666 -#: taiga/projects/notifications/models.py:73 -#: taiga/projects/notifications/models.py:90 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:64 taiga/projects/wiki/models.py:30 -#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 msgid "project" msgstr "專案" -#: taiga/projects/attachments/models.py:42 +#: taiga/projects/attachments/models.py:43 msgid "content type" msgstr "內容類型" -#: taiga/projects/attachments/models.py:44 +#: taiga/projects/attachments/models.py:45 msgid "object id" msgstr "物件ID" -#: taiga/projects/attachments/models.py:50 +#: taiga/projects/attachments/models.py:51 #: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/issues/models.py:57 taiga/projects/milestones/models.py:52 -#: taiga/projects/models.py:160 taiga/projects/models.py:692 -#: taiga/projects/tasks/models.py:50 taiga/projects/userstories/models.py:87 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:30 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 msgid "modified date" msgstr "修改日期" -#: taiga/projects/attachments/models.py:55 +#: taiga/projects/attachments/models.py:56 msgid "attached file" msgstr "附加檔案" -#: taiga/projects/attachments/models.py:57 +#: taiga/projects/attachments/models.py:58 msgid "sha1" msgstr "sha1" -#: taiga/projects/attachments/models.py:59 +#: taiga/projects/attachments/models.py:60 msgid "is deprecated" msgstr "棄用" -#: taiga/projects/attachments/models.py:61 +#: taiga/projects/attachments/models.py:62 #: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:482 -#: taiga/projects/models.py:519 taiga/projects/models.py:546 -#: taiga/projects/models.py:581 taiga/projects/models.py:604 -#: taiga/projects/models.py:629 taiga/projects/models.py:662 -#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 msgid "order" msgstr "次序" -#: taiga/projects/choices.py:22 +#: taiga/projects/choices.py:23 msgid "AppearIn" msgstr "AppearIn" -#: taiga/projects/choices.py:23 +#: taiga/projects/choices.py:24 msgid "Jitsi" msgstr "Jitsi" -#: taiga/projects/choices.py:24 +#: taiga/projects/choices.py:25 msgid "Custom" msgstr "自定" -#: taiga/projects/choices.py:25 +#: taiga/projects/choices.py:26 msgid "Talky" msgstr "Talky" -#: taiga/projects/choices.py:32 +#: taiga/projects/choices.py:35 msgid "This project is blocked due to payment failure" msgstr "" -#: taiga/projects/choices.py:33 +#: taiga/projects/choices.py:36 msgid "This project is blocked by admin staff" msgstr "" -#: taiga/projects/choices.py:34 +#: taiga/projects/choices.py:37 msgid "This project is blocked because the owner left" msgstr "" -#: taiga/projects/custom_attributes/choices.py:27 +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 msgid "Text" msgstr "單行文字" -#: taiga/projects/custom_attributes/choices.py:28 +#: taiga/projects/custom_attributes/choices.py:29 msgid "Multi-Line Text" msgstr "多行列文字" -#: taiga/projects/custom_attributes/choices.py:29 +#: taiga/projects/custom_attributes/choices.py:30 msgid "Date" msgstr "日期" -#: taiga/projects/custom_attributes/choices.py:30 +#: taiga/projects/custom_attributes/choices.py:31 msgid "Url" msgstr "" #: taiga/projects/custom_attributes/models.py:39 -#: taiga/projects/issues/models.py:47 +#: taiga/projects/issues/models.py:45 msgid "type" msgstr "類型" -#: taiga/projects/custom_attributes/models.py:88 +#: taiga/projects/custom_attributes/models.py:94 msgid "values" msgstr "價值" -#: taiga/projects/custom_attributes/models.py:98 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:36 +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 msgid "user story" msgstr "使用者故事" -#: taiga/projects/custom_attributes/models.py:113 +#: taiga/projects/custom_attributes/models.py:136 msgid "task" msgstr "任務 " -#: taiga/projects/custom_attributes/models.py:128 +#: taiga/projects/custom_attributes/models.py:152 msgid "issue" msgstr "問題 " -#: taiga/projects/custom_attributes/serializers.py:58 +#: taiga/projects/custom_attributes/validators.py:58 msgid "Already exists one with the same name." msgstr "已存在相同姓名" -#: taiga/projects/history/api.py:71 +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "狀態" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "主旨" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "顏色" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "指派給" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "客戶要求" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "團隊要求" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 msgid "Comment already deleted" msgstr "評論已刪除 " -#: taiga/projects/history/api.py:90 +#: taiga/projects/history/api.py:151 msgid "Comment not deleted" msgstr "不可刪除 之評論 " -#: taiga/projects/history/choices.py:27 +#: taiga/projects/history/choices.py:31 msgid "Change" msgstr "更改" -#: taiga/projects/history/choices.py:28 +#: taiga/projects/history/choices.py:32 msgid "Create" msgstr "創建" -#: taiga/projects/history/choices.py:29 +#: taiga/projects/history/choices.py:33 msgid "Delete" msgstr "刪除 " @@ -1582,7 +1595,7 @@ msgstr "移除 " #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 #: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 -#: taiga/projects/services/stats.py:54 taiga/projects/services/stats.py:55 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 msgid "Unassigned" msgstr "無指定" @@ -1629,95 +1642,75 @@ msgstr "來自:" msgid "To:" msgstr "給:" -#: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/wiki/models.py:34 +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 msgid "content" msgstr "內容" -#: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/mixins/blocked.py:32 +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 msgid "blocked note" msgstr "封鎖筆記" -#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/history/templatetags/functions.py:28 msgid "sprint" msgstr "衝刺任務" -#: taiga/projects/issues/api.py:158 +#: taiga/projects/issues/api.py:156 msgid "You don't have permissions to set this sprint to this issue." msgstr "您無權限設定此問題的衝刺任務" -#: taiga/projects/issues/api.py:162 +#: taiga/projects/issues/api.py:160 msgid "You don't have permissions to set this status to this issue." msgstr "您無權限設定此問題的狀態" -#: taiga/projects/issues/api.py:166 +#: taiga/projects/issues/api.py:164 msgid "You don't have permissions to set this severity to this issue." msgstr "您無權限設定此問題的嚴重性" -#: taiga/projects/issues/api.py:170 +#: taiga/projects/issues/api.py:168 msgid "You don't have permissions to set this priority to this issue." msgstr "您無權限設定此問題的優先性" -#: taiga/projects/issues/api.py:174 +#: taiga/projects/issues/api.py:172 msgid "You don't have permissions to set this type to this issue." msgstr "您無權限設定此問題的類型" -#: taiga/projects/issues/models.py:37 taiga/projects/tasks/models.py:36 -#: taiga/projects/userstories/models.py:59 -msgid "ref" -msgstr "ref" - -#: taiga/projects/issues/models.py:41 taiga/projects/tasks/models.py:40 -#: taiga/projects/userstories/models.py:69 -msgid "status" -msgstr "狀態" - -#: taiga/projects/issues/models.py:43 +#: taiga/projects/issues/models.py:41 msgid "severity" msgstr "嚴重性" -#: taiga/projects/issues/models.py:45 +#: taiga/projects/issues/models.py:43 msgid "priority" msgstr "優先性" -#: taiga/projects/issues/models.py:50 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:62 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 msgid "milestone" msgstr "里程碑" -#: taiga/projects/issues/models.py:59 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 msgid "finished date" msgstr "完成日期" -#: taiga/projects/issues/models.py:61 taiga/projects/tasks/models.py:54 -#: taiga/projects/userstories/models.py:91 -msgid "subject" -msgstr "主旨" - -#: taiga/projects/issues/models.py:65 taiga/projects/tasks/models.py:64 -#: taiga/projects/userstories/models.py:95 -msgid "assigned to" -msgstr "指派給" - -#: taiga/projects/issues/models.py:67 taiga/projects/tasks/models.py:68 -#: taiga/projects/userstories/models.py:105 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 msgid "external reference" msgstr "外部參考" -#: taiga/projects/likes/models.py:35 +#: taiga/projects/likes/models.py:36 msgid "Like" msgstr "喜歡" -#: taiga/projects/likes/models.py:36 +#: taiga/projects/likes/models.py:37 msgid "Likes" msgstr "喜歡" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148 -#: taiga/projects/models.py:480 taiga/projects/models.py:544 -#: taiga/projects/models.py:627 taiga/projects/models.py:685 -#: taiga/projects/wiki/models.py:32 taiga/users/admin.py:57 -#: taiga/users/models.py:294 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "代稱" @@ -1729,8 +1722,9 @@ msgstr "预計開始日期" msgid "estimated finish date" msgstr "預計完成日期" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484 -#: taiga/projects/models.py:548 taiga/projects/models.py:631 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 msgid "is closed" msgstr "被關閉" @@ -1742,120 +1736,132 @@ msgstr "disponibility" 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 "該用戶無衝刺任務 " +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" -#: taiga/projects/mixins/blocked.py:30 +#: taiga/projects/mixins/blocked.py:31 msgid "is blocked" msgstr "已封鎖" -#: taiga/projects/mixins/ordering.py:48 +#: taiga/projects/mixins/ordering.py:49 #, python-brace-format msgid "'{param}' parameter is mandatory" msgstr "'{param}' 參數為必要" -#: taiga/projects/mixins/ordering.py:52 +#: taiga/projects/mixins/ordering.py:53 msgid "'project' parameter is mandatory" msgstr "'project'參數為必要" -#: taiga/projects/models.py:78 +#: taiga/projects/models.py:75 msgid "email" msgstr "電子郵件" -#: taiga/projects/models.py:80 +#: taiga/projects/models.py:77 msgid "create at" msgstr "創建於" -#: taiga/projects/models.py:82 taiga/users/models.py:155 +#: taiga/projects/models.py:79 taiga/users/models.py:154 msgid "token" msgstr "代號" -#: taiga/projects/models.py:88 +#: taiga/projects/models.py:85 msgid "invitation extra text" msgstr "額外文案邀請" -#: taiga/projects/models.py:91 +#: taiga/projects/models.py:88 taiga/projects/models.py:734 msgid "user order" msgstr "使用者次序" -#: taiga/projects/models.py:101 +#: taiga/projects/models.py:104 msgid "The user is already member of the project" msgstr "使用者已是專案成員" -#: taiga/projects/models.py:116 -msgid "default points" -msgstr "預設點數" +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" -#: taiga/projects/models.py:120 +#: taiga/projects/models.py:115 msgid "default US status" msgstr "預設使用者故事狀態" -#: taiga/projects/models.py:124 +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "預設點數" + +#: taiga/projects/models.py:122 msgid "default task status" msgstr "預設任務狀態" -#: taiga/projects/models.py:127 +#: taiga/projects/models.py:125 msgid "default priority" msgstr "預設優先性" -#: taiga/projects/models.py:130 +#: taiga/projects/models.py:128 msgid "default severity" msgstr "預設嚴重性" -#: taiga/projects/models.py:134 +#: taiga/projects/models.py:132 msgid "default issue status" msgstr "預設問題狀態" -#: taiga/projects/models.py:138 +#: taiga/projects/models.py:136 msgid "default issue type" msgstr "預設議題類型" -#: taiga/projects/models.py:154 +#: taiga/projects/models.py:152 msgid "logo" msgstr "圖標" -#: taiga/projects/models.py:164 +#: taiga/projects/models.py:162 msgid "members" msgstr "成員" -#: taiga/projects/models.py:167 +#: taiga/projects/models.py:165 msgid "total of milestones" msgstr "全部里程碑" -#: taiga/projects/models.py:168 +#: taiga/projects/models.py:166 msgid "total story points" msgstr "全部故事點數" -#: taiga/projects/models.py:171 taiga/projects/models.py:698 +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 msgid "active backlog panel" msgstr "活躍的待辦任務優先表面板" -#: taiga/projects/models.py:173 taiga/projects/models.py:700 +#: taiga/projects/models.py:173 taiga/projects/models.py:749 msgid "active kanban panel" msgstr "活躍的看板式面板" -#: taiga/projects/models.py:175 taiga/projects/models.py:702 +#: taiga/projects/models.py:175 taiga/projects/models.py:751 msgid "active wiki panel" msgstr "活躍的維基面板" -#: taiga/projects/models.py:177 taiga/projects/models.py:704 +#: taiga/projects/models.py:177 taiga/projects/models.py:753 msgid "active issues panel" msgstr "活躍的問題面板" -#: taiga/projects/models.py:180 taiga/projects/models.py:707 +#: taiga/projects/models.py:180 taiga/projects/models.py:756 msgid "videoconference system" msgstr "視訊會議系統" -#: taiga/projects/models.py:182 taiga/projects/models.py:709 +#: taiga/projects/models.py:182 taiga/projects/models.py:758 msgid "videoconference extra data" msgstr "視訊會議額外資料" -#: taiga/projects/models.py:187 +#: taiga/projects/models.py:188 msgid "creation template" msgstr "創建模版" -#: taiga/projects/models.py:191 +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "私密" + +#: taiga/projects/models.py:193 msgid "anonymous permissions" msgstr "匿名權限" @@ -1863,169 +1869,251 @@ msgstr "匿名權限" msgid "user permissions" msgstr "使用者權限" -#: taiga/projects/models.py:198 taiga/users/admin.py:61 -msgid "is private" -msgstr "私密" - -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:198 msgid "is featured" msgstr " 受矚目的" -#: taiga/projects/models.py:204 +#: taiga/projects/models.py:201 msgid "is looking for people" msgstr "正在找人" -#: taiga/projects/models.py:206 +#: taiga/projects/models.py:203 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:218 -msgid "tags colors" -msgstr "標籤顏色" - -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:217 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:225 +#: taiga/projects/models.py:221 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65 +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "更新日期時間" -#: taiga/projects/models.py:232 taiga/projects/models.py:244 -#: taiga/projects/votes/models.py:29 +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 msgid "count" msgstr "數量" -#: taiga/projects/models.py:235 +#: taiga/projects/models.py:231 msgid "fans last week" msgstr "上週粉絲" -#: taiga/projects/models.py:238 +#: taiga/projects/models.py:234 msgid "fans last month" msgstr "上個月粉絲" -#: taiga/projects/models.py:241 +#: taiga/projects/models.py:237 msgid "fans last year" msgstr "去年粉絲" -#: taiga/projects/models.py:247 +#: taiga/projects/models.py:243 msgid "activity last week" msgstr "上週活躍成員" -#: taiga/projects/models.py:250 +#: taiga/projects/models.py:246 msgid "activity last month" msgstr "上月活躍成員" -#: taiga/projects/models.py:253 +#: taiga/projects/models.py:249 msgid "activity last year" msgstr "去年活躍成員" -#: taiga/projects/models.py:467 +#: taiga/projects/models.py:500 msgid "modules config" msgstr "模組設定" -#: taiga/projects/models.py:486 +#: taiga/projects/models.py:552 msgid "is archived" msgstr "已歸檔" -#: taiga/projects/models.py:488 taiga/projects/models.py:550 -#: taiga/projects/models.py:583 taiga/projects/models.py:606 -#: taiga/projects/models.py:633 taiga/projects/models.py:664 -#: taiga/users/models.py:140 -msgid "color" -msgstr "顏色" - -#: taiga/projects/models.py:490 +#: taiga/projects/models.py:556 msgid "work in progress limit" msgstr "工作進度限制" -#: taiga/projects/models.py:521 taiga/userstorage/models.py:32 +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 msgid "value" msgstr "價值" -#: taiga/projects/models.py:695 +#: taiga/projects/models.py:742 msgid "default owner's role" msgstr "預設所有者角色" -#: taiga/projects/models.py:711 +#: taiga/projects/models.py:760 msgid "default options" msgstr "預設選項" -#: taiga/projects/models.py:712 +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 msgid "us statuses" msgstr "我們狀況" -#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42 -#: taiga/projects/userstories/models.py:74 +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 msgid "points" msgstr "點數" -#: taiga/projects/models.py:714 +#: taiga/projects/models.py:764 msgid "task statuses" msgstr "任務狀況" -#: taiga/projects/models.py:715 +#: taiga/projects/models.py:765 msgid "issue statuses" msgstr "問題狀況" -#: taiga/projects/models.py:716 +#: taiga/projects/models.py:766 msgid "issue types" msgstr "問題類型" -#: taiga/projects/models.py:717 +#: taiga/projects/models.py:767 msgid "priorities" msgstr "優先性" -#: taiga/projects/models.py:718 +#: taiga/projects/models.py:768 msgid "severities" msgstr "嚴重性" -#: taiga/projects/models.py:719 +#: taiga/projects/models.py:769 msgid "roles" msgstr "角色" -#: taiga/projects/notifications/choices.py:29 +#: taiga/projects/notifications/choices.py:30 msgid "Involved" msgstr "相關涉入者" -#: taiga/projects/notifications/choices.py:30 +#: taiga/projects/notifications/choices.py:31 msgid "All" msgstr "所有" -#: taiga/projects/notifications/choices.py:31 +#: taiga/projects/notifications/choices.py:32 msgid "None" msgstr "無" -#: taiga/projects/notifications/models.py:63 +#: taiga/projects/notifications/models.py:64 msgid "created date time" msgstr "創建日期時間" -#: taiga/projects/notifications/models.py:67 +#: taiga/projects/notifications/models.py:68 msgid "history entries" msgstr "歷史輸入" -#: taiga/projects/notifications/models.py:70 +#: taiga/projects/notifications/models.py:71 msgid "notify users" msgstr "通知用戶" -#: taiga/projects/notifications/models.py:92 #: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 msgid "Watched" msgstr "已觀注" -#: taiga/projects/notifications/services.py:64 -#: taiga/projects/notifications/services.py:78 +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 msgid "Notify exists for specified user and project" msgstr "通知特定使用者與專案退出" -#: taiga/projects/notifications/services.py:427 +#: taiga/projects/notifications/services.py:426 msgid "Invalid value for notify level" msgstr "通知水平的無效值" +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + #: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 #, python-format msgid "" @@ -2747,141 +2835,135 @@ msgstr "" "\n" "[%(project)s] 刪除維基頁 \"%(page)s\"\n" -#: taiga/projects/notifications/validators.py:47 +#: taiga/projects/notifications/validators.py:48 msgid "Watchers contains invalid users" msgstr "監督者包含無效使用者" -#: taiga/projects/occ/mixins.py:36 +#: taiga/projects/occ/mixins.py:37 msgid "The version must be an integer" msgstr "版本須為整數值 " -#: taiga/projects/occ/mixins.py:59 +#: taiga/projects/occ/mixins.py:60 msgid "The version parameter is not valid" msgstr "本版本參數無效" -#: taiga/projects/occ/mixins.py:75 +#: taiga/projects/occ/mixins.py:76 msgid "The version doesn't match with the current one" msgstr "版本與目前使用不相符" -#: taiga/projects/occ/mixins.py:94 +#: taiga/projects/occ/mixins.py:95 msgid "version" msgstr "版本" -#: taiga/projects/permissions.py:40 +#: taiga/projects/permissions.py:44 msgid "" "You can't leave the project if you are the owner or there are no more admins" msgstr "" -#: taiga/projects/serializers.py:172 -msgid "Email address is already taken" -msgstr "電子郵件已使用" - -#: taiga/projects/serializers.py:184 -msgid "Invalid role for the project" -msgstr "專案無效的角色" - -#: taiga/projects/serializers.py:195 -msgid "The project owner must be admin." +#: taiga/projects/services/members.py:118 +msgid "Project without owner" msgstr "" -#: taiga/projects/serializers.py:198 -msgid "At least one user must be an active admin for this project." -msgstr "" - -#: taiga/projects/serializers.py:396 -msgid "Default options" -msgstr "預設選項" - -#: taiga/projects/serializers.py:397 -msgid "User story's statuses" -msgstr "使用者故事狀態" - -#: taiga/projects/serializers.py:398 -msgid "Points" -msgstr "點數" - -#: taiga/projects/serializers.py:399 -msgid "Task's statuses" -msgstr "任務狀態" - -#: taiga/projects/serializers.py:400 -msgid "Issue's statuses" -msgstr "問題狀態" - -#: taiga/projects/serializers.py:401 -msgid "Issue's types" -msgstr "問題類型" - -#: taiga/projects/serializers.py:402 -msgid "Priorities" -msgstr "優先性" - -#: taiga/projects/serializers.py:403 -msgid "Severities" -msgstr "嚴重性" - -#: taiga/projects/serializers.py:404 -msgid "Roles" -msgstr "角色" - -#: taiga/projects/services/members.py:116 +#: taiga/projects/services/members.py:123 msgid "You have reached your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/members.py:120 +#: taiga/projects/services/members.py:127 msgid "You have reached your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/projects.py:69 -#: taiga/projects/services/projects.py:106 taiga/users/services.py:582 +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 msgid "You can't have more private projects" msgstr "" -#: taiga/projects/services/projects.py:73 -#: taiga/projects/services/projects.py:110 taiga/users/services.py:585 +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 msgid "" "This project reaches your current limit of memberships for private projects" msgstr "" -#: taiga/projects/services/projects.py:77 -#: taiga/projects/services/projects.py:114 taiga/users/services.py:589 +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 msgid "You can't have more public projects" msgstr "" -#: taiga/projects/services/projects.py:81 -#: taiga/projects/services/projects.py:118 taiga/users/services.py:592 +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 msgid "" "This project reaches your current limit of memberships for public projects" msgstr "" -#: taiga/projects/services/stats.py:196 +#: taiga/projects/services/stats.py:197 msgid "Future sprint" msgstr "未來之衝刺" -#: taiga/projects/services/stats.py:216 +#: taiga/projects/services/stats.py:217 msgid "Project End" msgstr "專案結束" -#: taiga/projects/services/transfer.py:61 -#: taiga/projects/services/transfer.py:68 -#: taiga/projects/services/transfer.py:71 taiga/users/api.py:169 -#: taiga/users/api.py:174 +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 msgid "Token is invalid" msgstr "代號無效" -#: taiga/projects/services/transfer.py:66 +#: taiga/projects/services/transfer.py:67 msgid "Token has expired" msgstr "" -#: taiga/projects/tasks/api.py:113 taiga/projects/tasks/api.py:122 +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "標籤" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "標籤顏色" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 msgid "You don't have permissions to set this sprint to this task." msgstr "無權限更動此任務下的衝刺任務" -#: taiga/projects/tasks/api.py:116 +#: taiga/projects/tasks/api.py:100 msgid "You don't have permissions to set this user story to this task." msgstr "無權限更動此務下的使用者故事" -#: taiga/projects/tasks/api.py:119 +#: taiga/projects/tasks/api.py:103 msgid "You don't have permissions to set this status to this task." msgstr "無權限更動此任務下的狀態" @@ -2897,9 +2979,35 @@ msgstr "任務板次序" msgid "is iocaine" msgstr "挑戰全新任務" -#: taiga/projects/tasks/validators.py:12 -msgid "There's no task with that id" -msgstr "該用戶無任務 " +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" #: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 #: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 @@ -3277,12 +3385,12 @@ msgid "" msgstr "" #. Translators: Name of scrum project template. -#: taiga/projects/translations.py:29 +#: taiga/projects/translations.py:30 msgid "Scrum" msgstr "Scrum" #. Translators: Description of scrum project template. -#: taiga/projects/translations.py:31 +#: taiga/projects/translations.py:32 msgid "" "The agile product backlog in Scrum is a prioritized features list, " "containing short descriptions of all functionality desired in the product. " @@ -3296,12 +3404,12 @@ msgstr "" "客戶中學到的回應,加以改變或調整。" #. Translators: Name of kanban project template. -#: taiga/projects/translations.py:34 +#: taiga/projects/translations.py:35 msgid "Kanban" msgstr "Kanban" #. Translators: Description of kanban project template. -#: taiga/projects/translations.py:36 +#: taiga/projects/translations.py:37 msgid "" "Kanban is a method for managing knowledge work with an emphasis on just-in-" "time delivery while not overloading the team members. In this approach, the " @@ -3312,303 +3420,388 @@ msgstr "" "定義任務到其傳送給客戶的過程,以參與者可以看到且成員從次序排列上推動來呈現" #. Translators: User story point value (value = undefined) -#: taiga/projects/translations.py:44 +#: taiga/projects/translations.py:45 msgid "?" msgstr "?" #. Translators: User story point value (value = 0) -#: taiga/projects/translations.py:46 +#: taiga/projects/translations.py:47 msgid "0" msgstr "0" #. Translators: User story point value (value = 0.5) -#: taiga/projects/translations.py:48 +#: taiga/projects/translations.py:49 msgid "1/2" msgstr "1/2" #. Translators: User story point value (value = 1) -#: taiga/projects/translations.py:50 +#: taiga/projects/translations.py:51 msgid "1" msgstr "1" #. Translators: User story point value (value = 2) -#: taiga/projects/translations.py:52 +#: taiga/projects/translations.py:53 msgid "2" msgstr "2" #. Translators: User story point value (value = 3) -#: taiga/projects/translations.py:54 +#: taiga/projects/translations.py:55 msgid "3" msgstr "3" #. Translators: User story point value (value = 5) -#: taiga/projects/translations.py:56 +#: taiga/projects/translations.py:57 msgid "5" msgstr "5" #. Translators: User story point value (value = 8) -#: taiga/projects/translations.py:58 +#: taiga/projects/translations.py:59 msgid "8" msgstr "8" #. Translators: User story point value (value = 10) -#: taiga/projects/translations.py:60 +#: taiga/projects/translations.py:61 msgid "10" msgstr "10" #. Translators: User story point value (value = 13) -#: taiga/projects/translations.py:62 +#: taiga/projects/translations.py:63 msgid "13" msgstr "13" #. Translators: User story point value (value = 20) -#: taiga/projects/translations.py:64 +#: taiga/projects/translations.py:65 msgid "20" msgstr "20" #. Translators: User story point value (value = 40) -#: taiga/projects/translations.py:66 +#: taiga/projects/translations.py:67 msgid "40" msgstr "40" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:74 taiga/projects/translations.py:97 -#: taiga/projects/translations.py:113 +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 msgid "New" msgstr "新 " #. Translators: User story status -#: taiga/projects/translations.py:77 +#: taiga/projects/translations.py:78 msgid "Ready" msgstr "準備好了" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:80 taiga/projects/translations.py:99 -#: taiga/projects/translations.py:115 +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 msgid "In progress" msgstr "進行中" #. Translators: User story status #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:83 taiga/projects/translations.py:101 -#: taiga/projects/translations.py:117 +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 msgid "Ready for test" msgstr "準備測試 " #. Translators: User story status -#: taiga/projects/translations.py:86 +#: taiga/projects/translations.py:87 msgid "Done" msgstr "完成" #. Translators: User story status -#: taiga/projects/translations.py:89 +#: taiga/projects/translations.py:90 msgid "Archived" msgstr "歸檔" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:103 taiga/projects/translations.py:119 +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 msgid "Closed" msgstr "關閉" #. Translators: Task status #. Translators: Issue status -#: taiga/projects/translations.py:105 taiga/projects/translations.py:121 +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 msgid "Needs Info" msgstr "需求資訊" #. Translators: Issue status -#: taiga/projects/translations.py:123 +#: taiga/projects/translations.py:124 msgid "Postponed" msgstr "延後" #. Translators: Issue status -#: taiga/projects/translations.py:125 +#: taiga/projects/translations.py:126 msgid "Rejected" msgstr "拒絕 " #. Translators: Issue type -#: taiga/projects/translations.py:133 +#: taiga/projects/translations.py:134 msgid "Bug" msgstr "系統錯誤" #. Translators: Issue type -#: taiga/projects/translations.py:135 +#: taiga/projects/translations.py:136 msgid "Question" msgstr "問題" #. Translators: Issue type -#: taiga/projects/translations.py:137 +#: taiga/projects/translations.py:138 msgid "Enhancement" msgstr "強化" #. Translators: Issue priority -#: taiga/projects/translations.py:145 +#: taiga/projects/translations.py:146 msgid "Low" msgstr "低" #. Translators: Issue priority #. Translators: Issue severity -#: taiga/projects/translations.py:147 taiga/projects/translations.py:160 +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 msgid "Normal" msgstr "一般" #. Translators: Issue priority -#: taiga/projects/translations.py:149 +#: taiga/projects/translations.py:150 msgid "High" msgstr "高" #. Translators: Issue severity -#: taiga/projects/translations.py:156 +#: taiga/projects/translations.py:157 msgid "Wishlist" msgstr "願望清單" #. Translators: Issue severity -#: taiga/projects/translations.py:158 +#: taiga/projects/translations.py:159 msgid "Minor" msgstr "次要" #. Translators: Issue severity -#: taiga/projects/translations.py:162 +#: taiga/projects/translations.py:163 msgid "Important" msgstr "重要" #. Translators: Issue severity -#: taiga/projects/translations.py:164 +#: taiga/projects/translations.py:165 msgid "Critical" msgstr "關鍵" #. Translators: User role -#: taiga/projects/translations.py:171 +#: taiga/projects/translations.py:172 msgid "UX" msgstr "使用者介面" #. Translators: User role -#: taiga/projects/translations.py:173 +#: taiga/projects/translations.py:174 msgid "Design" msgstr "設計" #. Translators: User role -#: taiga/projects/translations.py:175 +#: taiga/projects/translations.py:176 msgid "Front" msgstr "前台" #. Translators: User role -#: taiga/projects/translations.py:177 +#: taiga/projects/translations.py:178 msgid "Back" msgstr "後台" #. Translators: User role -#: taiga/projects/translations.py:179 +#: taiga/projects/translations.py:180 msgid "Product Owner" msgstr "產品所有人" #. Translators: User role -#: taiga/projects/translations.py:181 +#: taiga/projects/translations.py:182 msgid "Stakeholder" msgstr "利害關係人" -#: taiga/projects/userstories/api.py:163 +#: taiga/projects/userstories/api.py:119 msgid "You don't have permissions to set this sprint to this user story." msgstr "無權限更動使用者故事的衝刺任務" -#: taiga/projects/userstories/api.py:167 +#: taiga/projects/userstories/api.py:123 msgid "You don't have permissions to set this status to this user story." msgstr "無權限更動此使用者故事的狀態" -#: taiga/projects/userstories/api.py:267 +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "産生使用者故事 #{ref} - {subject}" -#: taiga/projects/userstories/models.py:39 +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 msgid "role" msgstr "角色" -#: taiga/projects/userstories/models.py:77 +#: taiga/projects/userstories/models.py:79 msgid "backlog order" msgstr "待辦任務先後次序" -#: taiga/projects/userstories/models.py:79 #: taiga/projects/userstories/models.py:81 msgid "sprint order" msgstr "衝刺次序" -#: taiga/projects/userstories/models.py:89 +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 msgid "finish date" msgstr "完成日期" -#: taiga/projects/userstories/models.py:97 -msgid "is client requirement" -msgstr "客戶要求" - -#: taiga/projects/userstories/models.py:99 -msgid "is team requirement" -msgstr "團隊要求" - -#: taiga/projects/userstories/models.py:104 +#: taiga/projects/userstories/models.py:106 msgid "generated from issue" msgstr "産生自問題 " -#: taiga/projects/userstories/validators.py:29 +#: taiga/projects/userstories/validators.py:43 msgid "There's no user story with that id" msgstr "該ID無相關使用者故事" -#: taiga/projects/validators.py:29 +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 msgid "There's no project with that id" msgstr "該ID無相關專案" -#: taiga/projects/validators.py:38 -msgid "There's no user story status with that id" -msgstr "該ID無相關使用者故事狀態" +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "電子郵件已使用" -#: taiga/projects/validators.py:47 -msgid "There's no task status with that id" -msgstr "該ID無相關任務狀況" +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "專案無效的角色" -#: taiga/projects/votes/models.py:32 taiga/projects/votes/models.py:33 -#: taiga/projects/votes/models.py:57 +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "" + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "" + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "預設選項" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "使用者故事狀態" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "點數" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "任務狀態" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "問題狀態" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "問題類型" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "優先性" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "嚴重性" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "角色" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 msgid "Votes" msgstr "投票數" -#: taiga/projects/votes/models.py:56 +#: taiga/projects/votes/models.py:57 msgid "Vote" msgstr "投票 " -#: taiga/projects/wiki/api.py:70 +#: taiga/projects/wiki/api.py:77 msgid "'content' parameter is mandatory" msgstr "'content'參數為必要" -#: taiga/projects/wiki/api.py:73 +#: taiga/projects/wiki/api.py:80 msgid "'project_id' parameter is mandatory" msgstr "'project_id'參數為必要" -#: taiga/projects/wiki/models.py:38 +#: taiga/projects/wiki/models.py:41 msgid "last modifier" msgstr "上次更改" -#: taiga/projects/wiki/models.py:71 +#: taiga/projects/wiki/models.py:74 msgid "href" msgstr "href" -#: taiga/timeline/signals.py:68 +#: taiga/timeline/signals.py:63 msgid "Check the history API for the exact diff" msgstr "檢查API過去資料以找出差異" -#: taiga/users/admin.py:38 +#: taiga/users/admin.py:39 msgid "Project Member" msgstr "" -#: taiga/users/admin.py:39 +#: taiga/users/admin.py:40 msgid "Project Members" msgstr "" -#: taiga/users/admin.py:49 +#: taiga/users/admin.py:50 msgid "id" msgstr "" @@ -3636,145 +3829,137 @@ msgstr "" msgid "Important dates" msgstr "重要日期" -#: taiga/users/api.py:113 +#: taiga/users/api.py:123 msgid "Duplicated email" msgstr "複製電子郵件" -#: taiga/users/api.py:115 +#: taiga/users/api.py:125 msgid "Not valid email" msgstr "非有效電子郵性" -#: taiga/users/api.py:148 +#: taiga/users/api.py:165 msgid "Invalid username or email" msgstr "無效使用者或郵件" -#: taiga/users/api.py:157 +#: taiga/users/api.py:174 msgid "Mail sended successful!" msgstr "成功送出郵件" -#: taiga/users/api.py:195 +#: taiga/users/api.py:212 msgid "Current password parameter needed" msgstr "需要目前密碼之參數" -#: taiga/users/api.py:198 +#: taiga/users/api.py:215 msgid "New password parameter needed" msgstr "需要新密碼參數" -#: taiga/users/api.py:201 +#: taiga/users/api.py:218 msgid "Invalid password length at least 6 charaters needed" msgstr "無效密碼長度,至少需6個字元" -#: taiga/users/api.py:204 +#: taiga/users/api.py:221 msgid "Invalid current password" msgstr "無效密碼" -#: taiga/users/api.py:251 taiga/users/api.py:257 +#: taiga/users/api.py:268 taiga/users/api.py:274 msgid "" "Invalid, are you sure the token is correct and you didn't use it before?" msgstr "無效,請確認代號正確,之前是否曾使用過?" -#: taiga/users/api.py:284 taiga/users/api.py:292 taiga/users/api.py:295 +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 msgid "Invalid, are you sure the token is correct?" msgstr "無效,請確認代號是否正確?" -#: taiga/users/models.py:96 +#: taiga/users/models.py:95 msgid "superuser status" msgstr "超級使用者狀態 " -#: taiga/users/models.py:97 +#: taiga/users/models.py:96 msgid "" "Designates that this user has all permissions without explicitly assigning " "them." msgstr "無經明確分派,即賦予該使用者所有權限," -#: taiga/users/models.py:127 +#: taiga/users/models.py:126 msgid "username" msgstr "使用者名稱" -#: taiga/users/models.py:128 +#: taiga/users/models.py:127 msgid "" "Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" msgstr "必填。最多30字元(可為數字,字母,符號....)" -#: taiga/users/models.py:131 +#: taiga/users/models.py:130 msgid "Enter a valid username." msgstr "輸入有效的使用者名稱 " -#: taiga/users/models.py:134 +#: taiga/users/models.py:133 msgid "active" msgstr "活躍" -#: taiga/users/models.py:135 +#: taiga/users/models.py:134 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." msgstr "賦予該使用者活躍角色,以不選擇取代刪除帳戶功能。" -#: taiga/users/models.py:141 +#: taiga/users/models.py:140 msgid "biography" msgstr "自傳" -#: taiga/users/models.py:144 +#: taiga/users/models.py:143 msgid "photo" msgstr "照片" -#: taiga/users/models.py:145 +#: taiga/users/models.py:144 msgid "date joined" msgstr "加入日期" -#: taiga/users/models.py:147 +#: taiga/users/models.py:146 msgid "default language" msgstr "預設語言 " -#: taiga/users/models.py:149 +#: taiga/users/models.py:148 msgid "default theme" msgstr "預設主題" -#: taiga/users/models.py:151 +#: taiga/users/models.py:150 msgid "default timezone" msgstr "預設時區" -#: taiga/users/models.py:153 +#: taiga/users/models.py:152 msgid "colorize tags" msgstr "顏色標籤" -#: taiga/users/models.py:158 +#: taiga/users/models.py:157 msgid "email token" msgstr "電子郵件符號 " -#: taiga/users/models.py:160 +#: taiga/users/models.py:159 msgid "new email address" msgstr "新電子郵件地址" -#: taiga/users/models.py:167 +#: taiga/users/models.py:166 msgid "max number of owned private projects" msgstr "" -#: taiga/users/models.py:170 +#: taiga/users/models.py:169 msgid "max number of owned public projects" msgstr "" -#: taiga/users/models.py:173 +#: taiga/users/models.py:172 msgid "max number of memberships for each owned private project" msgstr "" -#: taiga/users/models.py:177 +#: taiga/users/models.py:176 msgid "max number of memberships for each owned public project" msgstr "" -#: taiga/users/models.py:297 +#: taiga/users/models.py:296 msgid "permissions" msgstr "許可" -#: taiga/users/serializers.py:65 -msgid "invalid" -msgstr "無效" - -#: taiga/users/serializers.py:76 -msgid "Invalid username. Try with a different one." -msgstr "無效使用者名稱,請重試其它名稱 " - -#: taiga/users/services.py:53 taiga/users/services.py:70 +#: taiga/users/services.py:51 taiga/users/services.py:68 msgid "Username or password does not matches user." msgstr "用戶名稱與密碼不符" @@ -3951,47 +4136,51 @@ msgstr "" msgid "You've been Taigatized!" msgstr "您已加入Taiga" -#: taiga/users/validators.py:30 -msgid "There's no role with that id" -msgstr "該用戶無角色" +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "無效" -#: taiga/userstorage/api.py:51 +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "無效使用者名稱,請重試其它名稱 " + +#: taiga/userstorage/api.py:53 msgid "" "Duplicate key value violates unique constraint. Key '{}' already exists." msgstr "複製的關鍵值侵害獨特約束 關鍵值'{}' 已存在" -#: taiga/userstorage/models.py:31 +#: taiga/userstorage/models.py:32 msgid "key" msgstr "關鍵值" -#: taiga/webhooks/models.py:29 taiga/webhooks/models.py:39 +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 msgid "URL" msgstr "URL" -#: taiga/webhooks/models.py:30 +#: taiga/webhooks/models.py:31 msgid "secret key" msgstr "袐密代碼" -#: taiga/webhooks/models.py:40 +#: taiga/webhooks/models.py:41 msgid "status code" msgstr "狀態碼" -#: taiga/webhooks/models.py:41 +#: taiga/webhooks/models.py:42 msgid "request data" msgstr "要求資料" -#: taiga/webhooks/models.py:42 +#: taiga/webhooks/models.py:43 msgid "request headers" msgstr "要求標頭" -#: taiga/webhooks/models.py:43 +#: taiga/webhooks/models.py:44 msgid "response data" msgstr "回應資料" -#: taiga/webhooks/models.py:44 +#: taiga/webhooks/models.py:45 msgid "response headers" msgstr "回應標頭 " -#: taiga/webhooks/models.py:45 +#: taiga/webhooks/models.py:46 msgid "duration" msgstr "期間" From f8a1c8f4a3280dd229d80a9bc3bc8dac99f76cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 20 Sep 2016 13:23:42 +0200 Subject: [PATCH 244/261] [i18n] Add norwegian Bokmal (nb) translation --- CHANGELOG.md | 2 + settings/common.py | 2 +- taiga/locale/nb/LC_MESSAGES/django.po | 3804 +++++++++++++++++++++++++ 3 files changed, 3807 insertions(+), 1 deletion(-) create mode 100644 taiga/locale/nb/LC_MESSAGES/django.po diff --git a/CHANGELOG.md b/CHANGELOG.md index af8bcd13..728ad58c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ - Add created-, modified-, finished- and finish_date queryset filters - Support exact match, gt, gte, lt, lte - added issues, tasks and userstories accordingly +- i18n: + - Add norwegian Bokmal (nb) translation. ### Misc - [API] Improve performance of some calls over list. diff --git a/settings/common.py b/settings/common.py index 7af70452..87d0d904 100644 --- a/settings/common.py +++ b/settings/common.py @@ -124,7 +124,7 @@ LANGUAGES = [ #("mn", "Монгол"), # Mongolian #("mr", "मराठी"), # Marathi #("my", "မြန်မာ"), # Burmese - #("nb", "Norsk (bokmål)"), # Norwegian Bokmal + ("nb", "Norsk (bokmål)"), # Norwegian Bokmal #("ne", "नेपाली"), # Nepali ("nl", "Nederlands"), # Dutch #("nn", "Norsk (nynorsk)"), # Norwegian Nynorsk diff --git a/taiga/locale/nb/LC_MESSAGES/django.po b/taiga/locale/nb/LC_MESSAGES/django.po new file mode 100644 index 00000000..cff071a0 --- /dev/null +++ b/taiga/locale/nb/LC_MESSAGES/django.po @@ -0,0 +1,3804 @@ +# taiga-back.taiga. +# Copyright (C) 2014-2016 Taiga Dev Team +# This file is distributed under the same license as the taiga-back package. +# +# Translators: +# Jørgen Skår Fischer , 2016 +msgid "" +msgstr "" +"Project-Id-Version: taiga-back\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"PO-Revision-Date: 2016-09-20 10:50+0000\n" +"Last-Translator: Taiga Dev Team \n" +"Language-Team: Norwegian Bokmål (http://www.transifex.com/taiga-agile-llc/" +"taiga-back/language/nb/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nb\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: taiga/auth/api.py:102 +msgid "Public register is disabled." +msgstr "Offentlig register er deaktivert." + +#: taiga/auth/api.py:135 +msgid "invalid register type" +msgstr "ugyldig registertype" + +#: taiga/auth/api.py:148 +msgid "invalid login type" +msgstr "ugyldig påloggingstype" + +#: taiga/auth/services.py:76 +msgid "Username is already in use." +msgstr "Brukernavnet er allerede i bruk." + +#: taiga/auth/services.py:79 +msgid "Email is already in use." +msgstr "Epostadressen er allerede i bruk." + +#: taiga/auth/services.py:95 +msgid "Token not matches any valid invitation." +msgstr "Poletten samvsarer ikke med noen gyldig invitasjon." + +#: taiga/auth/services.py:123 +msgid "User is already registered." +msgstr "Brukeren er allerede registrert." + +#: taiga/auth/services.py:147 +msgid "This user is already a member of the project." +msgstr "Denne brukeren er allerede et medlem i prosjektet." + +#: taiga/auth/services.py:173 +msgid "Error on creating new user." +msgstr "Feil ved å lage ny bruker." + +#: taiga/auth/tokens.py:49 taiga/auth/tokens.py:56 +#: taiga/external_apps/services.py:36 taiga/projects/api.py:364 +#: taiga/projects/api.py:385 +msgid "Invalid token" +msgstr "Ugyldig polett" + +#: taiga/auth/validators.py:37 taiga/users/validators.py:44 +msgid "invalid username" +msgstr "ugyldig brukernavn" + +#: taiga/auth/validators.py:42 taiga/users/validators.py:50 +msgid "" +"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'" +msgstr "Påkrevd. 255 tegn eller færre. Bokstaver, tall og /./-/_ tegn '" + +#: taiga/base/api/fields.py:294 +msgid "This field is required." +msgstr "Dette feltet er obligatorisk." + +#: taiga/base/api/fields.py:295 taiga/base/api/relations.py:337 +msgid "Invalid value." +msgstr "Ugyldig verdi." + +#: taiga/base/api/fields.py:479 +#, python-format +msgid "'%s' value must be either True or False." +msgstr "'%s' verdi må være enten 'True' eller 'False'" + +#: taiga/base/api/fields.py:543 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "" +"Skriv inn en gyldig 'slug' bestående av bokstaver, tall, understreker eller " +"bindestreker. " + +#: taiga/base/api/fields.py:558 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "Gjør et gyldig valg. %(value)s er ikke et av de tilgjengelige valgene." + +#: taiga/base/api/fields.py:621 +msgid "You email domain is not allowed" +msgstr "" + +#: taiga/base/api/fields.py:630 +msgid "Enter a valid email address." +msgstr "Skriv inn en gyldig epostadresse." + +#: taiga/base/api/fields.py:672 +#, python-format +msgid "Date has wrong format. Use one of these formats instead: %s" +msgstr "Datoen har feil format. Bruk en av disse formatene istedet: %s" + +#: taiga/base/api/fields.py:736 +#, python-format +msgid "Datetime has wrong format. Use one of these formats instead: %s" +msgstr "Datotid har feil format. Bruk en av disse formatene istedet: %s" + +#: taiga/base/api/fields.py:806 +#, python-format +msgid "Time has wrong format. Use one of these formats instead: %s" +msgstr "Tid har feil format. Bruk en av disse formatene istedet: %s" + +#: taiga/base/api/fields.py:863 +msgid "Enter a whole number." +msgstr "Skriv inn et heltall." + +#: taiga/base/api/fields.py:864 taiga/base/api/fields.py:917 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "Sikre at denne verdien er mindre enn eller lik %(limit_value)s." + +#: taiga/base/api/fields.py:865 taiga/base/api/fields.py:918 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "Sikre at denne verdien er større enn eller lik %(limit_value)s." + +#: taiga/base/api/fields.py:895 +#, python-format +msgid "\"%s\" value must be a float." +msgstr "\"%s\" verdi må være et desimaltall." + +#: taiga/base/api/fields.py:916 +msgid "Enter a number." +msgstr "Skriv inn et nummer." + +#: taiga/base/api/fields.py:919 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Pass på at det ikke er flere enn %s sifre totalt." + +#: taiga/base/api/fields.py:920 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Pass på at det ikke er flere enn %s desimaler." + +#: taiga/base/api/fields.py:921 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Pass på at det ikke er flere enn %s siffer før komma." + +#: taiga/base/api/fields.py:988 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "Ingen fil ble sendt. Kontroller kodingstypen på skjemaet." + +#: taiga/base/api/fields.py:989 +msgid "No file was submitted." +msgstr "Ingen fil ble sendt." + +#: taiga/base/api/fields.py:990 +msgid "The submitted file is empty." +msgstr "Den sendte filen er tom." + +#: taiga/base/api/fields.py:991 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "" +"Sikre at dette filnavnet har på det meste %(max)d tegn (det har %(length)d)." + +#: taiga/base/api/fields.py:992 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" +"Vennligst enten send inn en fil eller sjekk den klare sjekkboksen, ikke " +"begge deler." + +#: taiga/base/api/fields.py:1032 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Last opp et gyldig bilde. Filen du lastet opp var enten ikke et bilde eller " +"et ødelagt bilde." + +#: taiga/base/api/mixins.py:284 taiga/base/exceptions.py:211 +#: taiga/hooks/api.py:69 taiga/projects/api.py:396 taiga/projects/api.py:671 +#: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 +#: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 +#: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 +#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/webhooks/api.py:71 +msgid "Blocked element" +msgstr "Blokkert element" + +#: taiga/base/api/pagination.py:214 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "" +"Siden er ikke 'sist', og den kan heller ikke konverteres til en integer." + +#: taiga/base/api/pagination.py:218 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "Ugyldig side (%(page_number)s): %(message)s" + +#: taiga/base/api/permissions.py:66 +msgid "Invalid permission definition." +msgstr "Ugyldig tilgangsdefinisjon." + +#: taiga/base/api/relations.py:247 +#, python-format +msgid "Invalid pk '%s' - object does not exist." +msgstr "Ugyldig pk '%s' - objektet eksisterer ikke." + +#: taiga/base/api/relations.py:248 +#, python-format +msgid "Incorrect type. Expected pk value, received %s." +msgstr "Feil type. Forventet \"pk\" verdi, mottok %s." + +#: taiga/base/api/relations.py:336 +#, python-format +msgid "Object with %s=%s does not exist." +msgstr "Objekt med %s=%s eksisterer ikke" + +#: taiga/base/api/relations.py:372 +msgid "Invalid hyperlink - No URL match" +msgstr "Ugyldig " + +#: taiga/base/api/relations.py:373 +msgid "Invalid hyperlink - Incorrect URL match" +msgstr "Ugyldig hyperkobling - Feil URL" + +#: taiga/base/api/relations.py:374 +msgid "Invalid hyperlink due to configuration error" +msgstr "Ugyldig hyperkobling på grunn av konfigurasjonsfeil" + +#: taiga/base/api/relations.py:375 +msgid "Invalid hyperlink - object does not exist." +msgstr "Ugyldig hyperkobling - objekt finnes ikke." + +#: taiga/base/api/relations.py:376 +#, python-format +msgid "Incorrect type. Expected url string, received %s." +msgstr "Feil type. Forventet url streng, fikk %s." + +#: taiga/base/api/serializers.py:324 +msgid "Invalid data" +msgstr "Ugyldig data." + +#: taiga/base/api/serializers.py:416 +msgid "No input provided" +msgstr "Ingen inndata ble angitt" + +#: taiga/base/api/serializers.py:579 +msgid "Cannot create a new item, only existing items may be updated." +msgstr "" +"Kan ikke opprette et nytt element, kun eksisterende elementer kan oppdateres." + +#: taiga/base/api/serializers.py:590 +msgid "Expected a list of items." +msgstr "Forventet en liste med elementer." + +#: taiga/base/api/views.py:126 +msgid "Not found" +msgstr "Ikke funnet" + +#: taiga/base/api/views.py:129 +msgid "Permission denied" +msgstr "Tilgang nektet" + +#: taiga/base/api/views.py:477 +msgid "Server application error" +msgstr "Server programfeil" + +#: taiga/base/connectors/exceptions.py:26 +msgid "Connection error." +msgstr "Tilkoblingsfeil" + +#: taiga/base/exceptions.py:79 +msgid "Malformed request." +msgstr "Uriktig formatert forespørsel" + +#: taiga/base/exceptions.py:84 +msgid "Incorrect authentication credentials." +msgstr "Feil godkjenningsinformasjon." + +#: taiga/base/exceptions.py:89 +msgid "Authentication credentials were not provided." +msgstr "Autentiseringsopplysninger ble ikke gitt." + +#: taiga/base/exceptions.py:94 +msgid "You do not have permission to perform this action." +msgstr "Du har ikke tillatelse til å utføre denne handlingen." + +#: taiga/base/exceptions.py:99 +#, python-format +msgid "Method '%s' not allowed." +msgstr "Metode '%s' ikke tillatt." + +#: taiga/base/exceptions.py:107 +msgid "Could not satisfy the request's Accept header" +msgstr "Kunne ikke tilfredsstille forespørselens 'Accept header'" + +#: taiga/base/exceptions.py:116 +#, python-format +msgid "Unsupported media type '%s' in request." +msgstr "Uegnet medietype '%s' i forespørselen." + +#: taiga/base/exceptions.py:124 +msgid "Request was throttled." +msgstr "Forespørselen ble strupet." + +#: taiga/base/exceptions.py:125 +#, python-format +msgid "Expected available in %d second%s." +msgstr "Forventet tilgjengelig om %d second%s." + +#: taiga/base/exceptions.py:139 +msgid "Unexpected error" +msgstr "Uventet feil" + +#: taiga/base/exceptions.py:151 +msgid "Not found." +msgstr "Ikke funnet." + +#: taiga/base/exceptions.py:156 +msgid "Method not supported for this endpoint." +msgstr "Metode ikke støttet for dette endepunktet ." + +#: taiga/base/exceptions.py:164 taiga/base/exceptions.py:172 +msgid "Wrong arguments." +msgstr "Feil argumenter." + +#: taiga/base/exceptions.py:176 +msgid "Data validation error" +msgstr "Data valideringsfeil" + +#: taiga/base/exceptions.py:188 +msgid "Integrity Error for wrong or invalid arguments" +msgstr "Integritetsfeil for gale eller ugyldige argumenter" + +#: taiga/base/exceptions.py:195 +msgid "Precondition error" +msgstr "Forutsetningsfeil" + +#: taiga/base/exceptions.py:219 +msgid "No room left for more projects." +msgstr "Ingen plass igjen til nye prosjekter." + +#: taiga/base/filters.py:81 taiga/base/filters.py:462 +msgid "Error in filter params types." +msgstr "Feil i filterparameter typer" + +#: taiga/base/filters.py:135 taiga/base/filters.py:242 +#: taiga/projects/filters.py:64 +msgid "'project' must be an integer value." +msgstr "'project' må være et heltall" + +#: 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 "Følg oss på 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 "Skaff koden på 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 "Besøk vår webside" + +#: 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/base-body-html.jinja:423 +#: taiga/base/templates/emails/hero-body-html.jinja:397 +#: taiga/base/templates/emails/updates-body-html.jinja:459 +#, python-format +msgid "" +"\n" +" Taiga Support:\n" +" %(support_url)s\n" +"
\n" +" Contact us:\n" +" \n" +" %(support_email)s\n" +" \n" +"
\n" +" Mailing list:\n" +" \n" +" %(mailing_list_url)s\n" +" \n" +" " +msgstr "" + +#: taiga/base/templates/emails/hero-body-html.jinja:6 +msgid "You have been Taigatized" +msgstr "Du har blitt Taigatisert" + +#: 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] Oppdateringer" + +#: taiga/base/templates/emails/updates-body-html.jinja:417 +msgid "Updates" +msgstr "Oppdateringer" + +#: taiga/base/templates/emails/updates-body-html.jinja:423 +#, python-format +msgid "" +"\n" +"

comment:" +"

\n" +"

" +"%(comment)s

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

kommentar:" +"

\n" +"

" +"%(comment)s

\n" +" " + +#: taiga/base/templates/emails/updates-body-text.jinja:6 +#, python-format +msgid "" +"\n" +" Comment: %(comment)s\n" +" " +msgstr "" +"\n" +" Kommentar: %(comment)s\n" +" " + +#: taiga/export_import/api.py:127 +msgid "We needed at least one role" +msgstr "Vi trenger minst en rolle" + +#: taiga/export_import/api.py:323 +msgid "Needed dump file" +msgstr "Har behov for dump-fil" + +#: taiga/export_import/api.py:333 +msgid "Invalid dump format" +msgstr "Ugyldig fil-dump format" + +#: taiga/export_import/services/store.py:718 +#: taiga/export_import/services/store.py:736 +msgid "error importing project data" +msgstr "feil under import av prosjektdata" + +#: taiga/export_import/services/store.py:743 +msgid "error importing roles" +msgstr "feil under import av roller" + +#: taiga/export_import/services/store.py:748 +msgid "error importing memberships" +msgstr "feil under import av medlemskap" + +#: taiga/export_import/services/store.py:759 +msgid "error importing lists of project attributes" +msgstr "feil under import av prosjektegenskaper" + +#: taiga/export_import/services/store.py:763 +msgid "error importing default project attributes values" +msgstr "feil under import av standard prosjektegenskapverdier" + +#: taiga/export_import/services/store.py:774 +msgid "error importing custom attributes" +msgstr "feil under import av egendefinerte egenskaper" + +#: taiga/export_import/services/store.py:778 +msgid "error importing sprints" +msgstr "feil under import av sprinter" + +#: taiga/export_import/services/store.py:782 +msgid "error importing issues" +msgstr "feil ved import av hendelser" + +#: taiga/export_import/services/store.py:786 +msgid "error importing user stories" +msgstr "feil ved import av brukerhistorier" + +#: taiga/export_import/services/store.py:790 +msgid "error importing epics" +msgstr "" + +#: taiga/export_import/services/store.py:794 +msgid "error importing tasks" +msgstr "feil ved import av oppgaver" + +#: taiga/export_import/services/store.py:798 +msgid "error importing wiki pages" +msgstr "feil ved import av wiki-sider" + +#: taiga/export_import/services/store.py:802 +msgid "error importing wiki links" +msgstr "feil ved import av wiki-lenker" + +#: taiga/export_import/services/store.py:806 +msgid "error importing tags" +msgstr "feil ved import av etiketter" + +#: taiga/export_import/services/store.py:810 +msgid "error importing timelines" +msgstr "feil ved import av tidslinjer" + +#: taiga/export_import/services/store.py:832 +msgid "unexpected error importing project" +msgstr "uventet feil ved import av prosjekt" + +#: taiga/export_import/tasks.py:62 taiga/export_import/tasks.py:63 +msgid "Error generating project dump" +msgstr "Feil ved generering av prosjektet dump" + +#: taiga/export_import/tasks.py:91 +#, python-brace-format +msgid "" +"\n" +"\n" +"Error loading dump by {user_full_name} <{user_email}>:\"\n" +"\n" +"\n" +"REASON:\n" +"-------\n" +"{reason}\n" +"\n" +"DETAILS:\n" +"--------\n" +"{details}\n" +"\n" +"TRACE ERROR:\n" +"------------" +msgstr "" + +#: taiga/export_import/tasks.py:120 +msgid "Error loading project dump" +msgstr "Feil ved lasting av prosjektet dump" + +#: taiga/export_import/tasks.py:121 +msgid "Error loading your project dump file" +msgstr "Feil ved lasting av din prosjektdump-fil" + +#: taiga/export_import/tasks.py:135 +msgid " -- no detail info --" +msgstr "-- ingen detaljeinfo --" + +#: taiga/export_import/templates/emails/dump_project-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump generated

\n" +"

Hello %(user)s,

\n" +"

Your dump from project %(project)s has been correctly generated.\n" +"

You can download it here:

\n" +" Download the dump file\n" +"

This file will be deleted on %(deletion_date)s.

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/export_import/templates/emails/dump_project-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your dump from project %(project)s has been correctly generated. You can " +"download it here:\n" +"\n" +"%(url)s\n" +"\n" +"This file will be deleted on %(deletion_date)s.\n" +"\n" +"---\n" +"The Taiga Team\n" +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-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project %(project)s has not been exported correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/export_import/templates/emails/export_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"Your project %(project)s has not been exported correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +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-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

%(error_message)s

\n" +"

Hello %(user)s,

\n" +"

Your project has not been importer correctly.

\n" +"

The Taiga system administrators have been informed.
Please, try " +"it again or contact with the support team at\n" +" %(support_email)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/export_import/templates/emails/import_error-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"%(error_message)s\n" +"\n" +"Your project has not been importer correctly.\n" +"\n" +"The Taiga system administrators have been informed.\n" +"\n" +"Please, try it again or contact with the support team at %(support_email)s\n" +"\n" +"---\n" +"The Taiga Team\n" +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-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Project dump imported

\n" +"

Hello %(user)s,

\n" +"

Your project dump has been correctly imported.

\n" +" Go to %(project)s\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/export_import/templates/emails/load_dump-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hello %(user)s,\n" +"\n" +"Your project dump has been correctly imported.\n" +"\n" +"You can see the project %(project)s here:\n" +"\n" +"%(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/export_import/templates/emails/load_dump-subject.jinja:1 +#, python-format +msgid "[%(project)s] Your project dump has been imported" +msgstr "" + +#: taiga/export_import/validators/fields.py:144 +msgid "{}=\"{}\" not found in this project" +msgstr "{}=\"{}\" ble ikke funnet i dette prosjektet" + +#: taiga/export_import/validators/validators.py:150 +#: taiga/projects/custom_attributes/validators.py:109 +msgid "Invalid content. It must be {\"key\": \"value\",...}" +msgstr "Ugyldig innhold. Det må være {\"key\": \"value\",...}" + +#: taiga/export_import/validators/validators.py:165 +#: taiga/projects/custom_attributes/validators.py:124 +msgid "It contain invalid custom fields." +msgstr "Den inneholder ugyldige egendefinerte feilter" + +#: taiga/export_import/validators/validators.py:245 +#: taiga/projects/validators.py:52 +msgid "Name duplicated for the project" +msgstr "Navnet er duplisert for prosjektet" + +#: taiga/external_apps/api.py:43 taiga/external_apps/api.py:70 +#: taiga/external_apps/api.py:77 +msgid "Authentication required" +msgstr "Autentisering kreves" + +#: taiga/external_apps/models.py:35 +#: taiga/projects/custom_attributes/models.py:35 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 +#: taiga/projects/models.py:511 taiga/projects/models.py:544 +#: taiga/projects/models.py:580 taiga/projects/models.py:602 +#: taiga/projects/models.py:636 taiga/projects/models.py:656 +#: taiga/projects/models.py:676 taiga/projects/models.py:708 +#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/users/models.py:292 taiga/webhooks/models.py:29 +msgid "name" +msgstr "navn" + +#: taiga/external_apps/models.py:37 +msgid "Icon url" +msgstr "Ikon url" + +#: taiga/external_apps/models.py:38 +msgid "web" +msgstr "web" + +#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/epics/models.py:54 +#: taiga/projects/history/templatetags/functions.py:25 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 +#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 +#: taiga/projects/userstories/models.py:94 +msgid "description" +msgstr "beskrivelse" + +#: taiga/external_apps/models.py:41 +msgid "Next url" +msgstr "Neste url" + +#: taiga/external_apps/models.py:43 +msgid "secret key for ciphering the application tokens" +msgstr "" + +#: taiga/external_apps/models.py:57 taiga/projects/likes/models.py:31 +#: taiga/projects/notifications/models.py:87 taiga/projects/votes/models.py:52 +msgid "user" +msgstr "bruker" + +#: taiga/external_apps/models.py:61 +msgid "application" +msgstr "applikasjon" + +#: taiga/feedback/models.py:25 taiga/users/models.py:137 +msgid "full name" +msgstr "fullt navn" + +#: taiga/feedback/models.py:27 taiga/users/models.py:132 +msgid "email address" +msgstr "epostadresse" + +#: taiga/feedback/models.py:29 +msgid "comment" +msgstr "kommentar" + +#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 +#: taiga/projects/custom_attributes/models.py:45 +#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 +#: taiga/projects/models.py:155 taiga/projects/models.py:736 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 +#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +msgid "created date" +msgstr "opprettet dato" + +#: 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" +"

Tilbakemelding

\n" +"

Taiga har mottatt tilbakemelding fra %(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" +"

Kommentar

\n" +"

%(comment)s

\n" +" " + +#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18 +#: taiga/projects/admin.py:106 taiga/users/admin.py:120 +msgid "Extra info" +msgstr "Ekstra info" + +#: 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" +"- Fra: %(full_name)s <%(email)s>\n" +"---------\n" +"- Kommentar:\n" +"%(comment)s\n" +"---------" + +#: taiga/feedback/templates/emails/feedback_notification-body-text.jinja:8 +msgid "- Extra info:" +msgstr "- Ekstra info: " + +#: 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] Tilbakemelding fra %(full_name)s <%(email)s>\n" + +#: taiga/hooks/api.py:54 +msgid "The payload is not a valid json" +msgstr "Payloaden er ikke gyldig json" + +#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 +#: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 +#: taiga/projects/userstories/api.py:268 +msgid "The project doesn't exist" +msgstr "Prosjektet eksisterer ikke" + +#: taiga/hooks/api.py:66 +msgid "Bad signature" +msgstr "Dårlig signatur" + +#: taiga/hooks/event_hooks.py:66 +#, python-brace-format +msgid "" +"[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\") says in " +"[{platform}#{number}]({comment_url} \"Go to comment\"):\n" +"\n" +"\"{comment_message}\"" +msgstr "" + +#: taiga/hooks/event_hooks.py:71 +#, python-brace-format +msgid "" +"Comment From {platform}:\n" +"\n" +"> {comment_message}" +msgstr "" + +#: taiga/hooks/event_hooks.py:84 +msgid "Invalid issue comment information" +msgstr "" + +#: taiga/hooks/event_hooks.py:103 +#, python-brace-format +msgid "" +"Issue created by [@{user_name}]({user_url} \"See @{user_name}'s {platform} " +"profile\") from [{platform}#{number}]({url} \"Go to issue\")." +msgstr "" + +#: taiga/hooks/event_hooks.py:107 +#, python-brace-format +msgid "Issue created from {platform}." +msgstr "" + +#: taiga/hooks/event_hooks.py:120 +msgid "Invalid issue information" +msgstr "Ugyldig hendelsesinformasjon" + +#: taiga/hooks/event_hooks.py:149 taiga/hooks/event_hooks.py:171 +msgid "unknown user" +msgstr "" + +#: taiga/hooks/event_hooks.py:156 +#, python-brace-format +msgid "" +"{user_text} changed the status from [{platform} commit]({commit_url} \"See " +"commit '{commit_id} - {commit_message}'\")\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" +msgstr "" + +#: taiga/hooks/event_hooks.py:161 +#, python-brace-format +msgid "" +"Changed status from {platform} commit.\n" +"\n" +" - Status: **{src_status}** → **{dst_status}**" +msgstr "" + +#: taiga/hooks/event_hooks.py:179 +#, python-brace-format +msgid "" +"This {type_name} has been mentioned by {user_text} in the [{platform} commit]" +"({commit_url} \"See commit '{commit_id} - {commit_message}'\") " +"\"{commit_message}\"" +msgstr "" + +#: taiga/hooks/event_hooks.py:184 +#, python-brace-format +msgid "" +"This issue has been mentioned in the {platform} commit \"{commit_message}\"" +msgstr "" + +#: taiga/hooks/event_hooks.py:206 +msgid "The referenced element doesn't exist" +msgstr "Det refererte elementet finnes ikke" + +#: taiga/hooks/event_hooks.py:222 +msgid "The status doesn't exist" +msgstr "Statusen eksisterer ikke" + +#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 +msgid "View project" +msgstr "Vis prosjekt" + +#: taiga/permissions/choices.py:24 taiga/permissions/choices.py:36 +msgid "View milestones" +msgstr "Vis milepæler" + +#: taiga/permissions/choices.py:25 taiga/permissions/choices.py:41 +msgid "View epic" +msgstr "" + +#: taiga/permissions/choices.py:26 +msgid "View user stories" +msgstr "Vis brukerhistorier" + +#: taiga/permissions/choices.py:27 taiga/permissions/choices.py:53 +msgid "View tasks" +msgstr "Vis oppgaver" + +#: taiga/permissions/choices.py:28 taiga/permissions/choices.py:59 +msgid "View issues" +msgstr "Vis hendelser" + +#: taiga/permissions/choices.py:29 taiga/permissions/choices.py:65 +msgid "View wiki pages" +msgstr "Se wiki-sider" + +#: taiga/permissions/choices.py:30 taiga/permissions/choices.py:71 +msgid "View wiki links" +msgstr "Se wiki-lenker" + +#: taiga/permissions/choices.py:37 +msgid "Add milestone" +msgstr "Legg til milepæl" + +#: taiga/permissions/choices.py:38 +msgid "Modify milestone" +msgstr "Endre milepæl" + +#: taiga/permissions/choices.py:39 +msgid "Delete milestone" +msgstr "Slett milepæl" + +#: taiga/permissions/choices.py:42 +msgid "Add epic" +msgstr "" + +#: taiga/permissions/choices.py:43 +msgid "Modify epic" +msgstr "" + +#: taiga/permissions/choices.py:44 +msgid "Comment epic" +msgstr "" + +#: taiga/permissions/choices.py:45 +msgid "Delete epic" +msgstr "" + +#: taiga/permissions/choices.py:47 +msgid "View user story" +msgstr "Vis brukerhistorie" + +#: taiga/permissions/choices.py:48 +msgid "Add user story" +msgstr "Legg til brukerhistorie" + +#: taiga/permissions/choices.py:49 +msgid "Modify user story" +msgstr "Rediger brukerhistorie" + +#: taiga/permissions/choices.py:50 +msgid "Comment user story" +msgstr "" + +#: taiga/permissions/choices.py:51 +msgid "Delete user story" +msgstr "Slett brukerhistorie" + +#: taiga/permissions/choices.py:54 +msgid "Add task" +msgstr "Legg til oppgave" + +#: taiga/permissions/choices.py:55 +msgid "Modify task" +msgstr "Rediger oppgave" + +#: taiga/permissions/choices.py:56 +msgid "Comment task" +msgstr "" + +#: taiga/permissions/choices.py:57 +msgid "Delete task" +msgstr "Slett oppgave" + +#: taiga/permissions/choices.py:60 +msgid "Add issue" +msgstr "Legg til hendelse" + +#: taiga/permissions/choices.py:61 +msgid "Modify issue" +msgstr "Rediger hendelse" + +#: taiga/permissions/choices.py:62 +msgid "Comment issue" +msgstr "" + +#: taiga/permissions/choices.py:63 +msgid "Delete issue" +msgstr "Slett hendelse" + +#: taiga/permissions/choices.py:66 +msgid "Add wiki page" +msgstr "Legg til wiki-side" + +#: taiga/permissions/choices.py:67 +msgid "Modify wiki page" +msgstr "Endre wiki-side" + +#: taiga/permissions/choices.py:68 +msgid "Comment wiki page" +msgstr "" + +#: taiga/permissions/choices.py:69 +msgid "Delete wiki page" +msgstr "Slett wiki-side" + +#: taiga/permissions/choices.py:72 +msgid "Add wiki link" +msgstr "Legg til wiki-lenke" + +#: taiga/permissions/choices.py:73 +msgid "Modify wiki link" +msgstr "Endre wiki-lenke" + +#: taiga/permissions/choices.py:74 +msgid "Delete wiki link" +msgstr "Slett wiki-lenke" + +#: taiga/permissions/choices.py:78 +msgid "Modify project" +msgstr "Rediger prosjekt" + +#: taiga/permissions/choices.py:79 +msgid "Delete project" +msgstr "Slett prosjekt" + +#: taiga/permissions/choices.py:80 +msgid "Add member" +msgstr "Legg til medlem" + +#: taiga/permissions/choices.py:81 +msgid "Remove member" +msgstr "Fjern medlem" + +#: taiga/permissions/choices.py:82 +msgid "Admin project values" +msgstr "Admin prosjektverdier" + +#: taiga/permissions/choices.py:83 +msgid "Admin roles" +msgstr "Admin roller" + +#: taiga/projects/admin.py:100 +msgid "Privacity" +msgstr "" + +#: taiga/projects/admin.py:112 +msgid "Modules" +msgstr "" + +#: taiga/projects/admin.py:120 +msgid "Default values" +msgstr "" + +#: taiga/projects/admin.py:126 +msgid "Activity" +msgstr "" + +#: taiga/projects/admin.py:131 +msgid "Fans" +msgstr "" + +#: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 +#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 +#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/users/admin.py:69 taiga/userstorage/models.py:27 +msgid "owner" +msgstr "eier" + +#: taiga/projects/admin.py:200 +#, python-brace-format +msgid "{count} successfully made public." +msgstr "" + +#: taiga/projects/admin.py:201 +msgid "Make public" +msgstr "" + +#: taiga/projects/admin.py:215 +#, python-brace-format +msgid "{count} successfully made private." +msgstr "" + +#: taiga/projects/admin.py:216 +msgid "Make private" +msgstr "" + +#: taiga/projects/admin.py:246 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "" + +#: taiga/projects/api.py:150 taiga/users/api.py:237 +msgid "Incomplete arguments" +msgstr "Ufullstendige argumenter" + +#: taiga/projects/api.py:154 taiga/users/api.py:242 +msgid "Invalid image format" +msgstr "Ugyldig bildeformat" + +#: taiga/projects/api.py:215 +msgid "Not valid template name" +msgstr "Ikke et gyldig malnavn" + +#: taiga/projects/api.py:218 +msgid "Not valid template description" +msgstr "Ikke en gyldig malbeskrivelse" + +#: taiga/projects/api.py:344 +msgid "Invalid user id" +msgstr "Ugyldig brukerid" + +#: taiga/projects/api.py:350 +msgid "The user doesn't exist" +msgstr "Brukeren eksisterer ikke" + +#: taiga/projects/api.py:354 +msgid "The user must be already a project member" +msgstr "Brukeren må allerede være et medlem i et prosjekt" + +#: taiga/projects/api.py:701 +msgid "" +"The project must have an owner and at least one of the users must be an " +"active admin" +msgstr "" +"Prosjektet må ha en eier og minst en av brukerne må være en aktiv " +"administrator" + +#: taiga/projects/api.py:735 +msgid "You don't have permisions to see that." +msgstr "Du har ikke tillatelser til å se det." + +#: taiga/projects/attachments/api.py:54 +msgid "Partial updates are not supported" +msgstr "Delvis oppdateringer støttes ikke" + +#: taiga/projects/attachments/api.py:69 +msgid "Object id issue isn't exists" +msgstr "" + +#: taiga/projects/attachments/api.py:72 +msgid "Project ID not matches between object and project" +msgstr "Prosjekt ID matcher ikke mellom objekt og prosjekt" + +#: taiga/projects/attachments/models.py:41 +#: taiga/projects/custom_attributes/models.py:42 +#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 +#: taiga/projects/models.py:521 taiga/projects/models.py:558 +#: taiga/projects/models.py:586 taiga/projects/models.py:612 +#: taiga/projects/models.py:642 taiga/projects/models.py:662 +#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/notifications/models.py:74 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 +#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 +#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +msgid "project" +msgstr "prosjekt" + +#: taiga/projects/attachments/models.py:43 +msgid "content type" +msgstr "innholdstype" + +#: taiga/projects/attachments/models.py:45 +msgid "object id" +msgstr "objektid" + +#: taiga/projects/attachments/models.py:51 +#: taiga/projects/custom_attributes/models.py:47 +#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 +#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 +#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/userstorage/models.py:31 +msgid "modified date" +msgstr "redigeringsdato" + +#: taiga/projects/attachments/models.py:56 +msgid "attached file" +msgstr "vedlagt fil" + +#: taiga/projects/attachments/models.py:58 +msgid "sha1" +msgstr "sha1" + +#: taiga/projects/attachments/models.py:60 +msgid "is deprecated" +msgstr "er foreldet" + +#: taiga/projects/attachments/models.py:62 +#: taiga/projects/custom_attributes/models.py:40 +#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:515 taiga/projects/models.py:548 +#: taiga/projects/models.py:582 taiga/projects/models.py:606 +#: taiga/projects/models.py:638 taiga/projects/models.py:658 +#: taiga/projects/models.py:680 taiga/projects/models.py:710 +#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +msgid "order" +msgstr "rekkefølge" + +#: taiga/projects/choices.py:23 +msgid "AppearIn" +msgstr "Vises i" + +#: taiga/projects/choices.py:24 +msgid "Jitsi" +msgstr "Jitsi" + +#: taiga/projects/choices.py:25 +msgid "Custom" +msgstr "Egendefinert" + +#: taiga/projects/choices.py:26 +msgid "Talky" +msgstr "Talky" + +#: taiga/projects/choices.py:35 +msgid "This project is blocked due to payment failure" +msgstr "Dette prosjektet er blokkert på grunn av manglende betaling" + +#: taiga/projects/choices.py:36 +msgid "This project is blocked by admin staff" +msgstr "Dette prosjektet er blokkert av en administrator" + +#: taiga/projects/choices.py:37 +msgid "This project is blocked because the owner left" +msgstr "Dette prosjektet er blokkert fordi eieren stakk" + +#: taiga/projects/choices.py:38 +msgid "This project is blocked while it's deleted" +msgstr "" + +#: taiga/projects/custom_attributes/choices.py:28 +msgid "Text" +msgstr "Tekst" + +#: taiga/projects/custom_attributes/choices.py:29 +msgid "Multi-Line Text" +msgstr "Tekst med flere linjer" + +#: taiga/projects/custom_attributes/choices.py:30 +msgid "Date" +msgstr "Dato" + +#: taiga/projects/custom_attributes/choices.py:31 +msgid "Url" +msgstr "Url" + +#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/issues/models.py:45 +msgid "type" +msgstr "type" + +#: taiga/projects/custom_attributes/models.py:94 +msgid "values" +msgstr "verdier" + +#: taiga/projects/custom_attributes/models.py:104 +msgid "epic" +msgstr "" + +#: taiga/projects/custom_attributes/models.py:120 +#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +msgid "user story" +msgstr "brukerhistorie" + +#: taiga/projects/custom_attributes/models.py:136 +msgid "task" +msgstr "oppgave" + +#: taiga/projects/custom_attributes/models.py:152 +msgid "issue" +msgstr "hendelse" + +#: taiga/projects/custom_attributes/validators.py:58 +msgid "Already exists one with the same name." +msgstr "Det finnes allerede en med samme navn." + +#: taiga/projects/epics/api.py:92 +msgid "You don't have permissions to set this status to this epic." +msgstr "" + +#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +msgid "ref" +msgstr "ref" + +#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +msgid "status" +msgstr "status" + +#: taiga/projects/epics/models.py:44 +msgid "epics order" +msgstr "" + +#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +msgid "subject" +msgstr "subjekt" + +#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 +#: taiga/projects/models.py:554 taiga/projects/models.py:610 +#: taiga/projects/models.py:640 taiga/projects/models.py:660 +#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/users/models.py:139 +msgid "color" +msgstr "farge" + +#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +msgid "assigned to" +msgstr "tildelt til" + +#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +msgid "is client requirement" +msgstr "Er klientkrav" + +#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +msgid "is team requirement" +msgstr "Er team behov" + +#: taiga/projects/epics/models.py:68 +msgid "user stories" +msgstr "" + +#: taiga/projects/epics/validators.py:37 +msgid "There's no epic with that id" +msgstr "" + +#: taiga/projects/history/api.py:93 +msgid "comment is required" +msgstr "" + +#: taiga/projects/history/api.py:96 +msgid "deleted comments can't be edited" +msgstr "" + +#: taiga/projects/history/api.py:130 +msgid "Comment already deleted" +msgstr "Kommentaren er allerede slettet" + +#: taiga/projects/history/api.py:151 +msgid "Comment not deleted" +msgstr "Kommentaren er ikke slettet" + +#: taiga/projects/history/choices.py:31 +msgid "Change" +msgstr "Endre" + +#: taiga/projects/history/choices.py:32 +msgid "Create" +msgstr "Opprett" + +#: taiga/projects/history/choices.py:33 +msgid "Delete" +msgstr "Slett" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:23 +#, python-format +msgid "%(role)s role points" +msgstr "%(role)s rollepoeng" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:26 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:131 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:134 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:157 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:194 +msgid "from" +msgstr "fra" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:32 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:142 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:145 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:163 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:180 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:200 +msgid "to" +msgstr "til" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:44 +msgid "Added new attachment" +msgstr "La til nytt vedlegg" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:62 +msgid "Updated attachment" +msgstr "Oppdatert vedlegg" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:68 +msgid "deprecated" +msgstr "foreldet" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:70 +msgid "not deprecated" +msgstr "ikke foreldet" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:86 +msgid "Deleted attachment" +msgstr "Slettede vedlegg" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:105 +msgid "added" +msgstr "lagt til" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:110 +msgid "removed" +msgstr "fjernet" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:135 +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:146 +#: taiga/projects/services/stats.py:55 taiga/projects/services/stats.py:56 +msgid "Unassigned" +msgstr "Ikke tildelt" + +#: taiga/projects/history/templates/emails/includes/fields_diff-html.jinja:212 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:87 +msgid "-deleted-" +msgstr "-slettet-" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:21 +msgid "to:" +msgstr "til:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:21 +msgid "from:" +msgstr "fra:" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:27 +msgid "Added" +msgstr "Lagt til" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:34 +msgid "Changed" +msgstr "Endret" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:41 +msgid "Deleted" +msgstr "Slettet" + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:55 +msgid "added:" +msgstr "lagt til: " + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:58 +msgid "removed:" +msgstr "fjernet: " + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:63 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:80 +msgid "From:" +msgstr "Fra: " + +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:64 +#: taiga/projects/history/templates/emails/includes/fields_diff-text.jinja:81 +msgid "To:" +msgstr "Til: " + +#: taiga/projects/history/templatetags/functions.py:26 +#: taiga/projects/wiki/models.py:37 +msgid "content" +msgstr "innhold" + +#: taiga/projects/history/templatetags/functions.py:27 +#: taiga/projects/mixins/blocked.py:33 +msgid "blocked note" +msgstr "blokkert notat" + +#: taiga/projects/history/templatetags/functions.py:28 +msgid "sprint" +msgstr "sprint" + +#: taiga/projects/issues/api.py:156 +msgid "You don't have permissions to set this sprint to this issue." +msgstr "Du har ikke tillatelse til å sette denne sprinten til denne hendelsen." + +#: taiga/projects/issues/api.py:160 +msgid "You don't have permissions to set this status to this issue." +msgstr "Du har ikke tillatelse til å sette denne statusen til denne hendelsen." + +#: taiga/projects/issues/api.py:164 +msgid "You don't have permissions to set this severity to this issue." +msgstr "" +"Du har ikke tillatelse til å sette denne alvorlighetsgraden til denne " +"hendelsen." + +#: taiga/projects/issues/api.py:168 +msgid "You don't have permissions to set this priority to this issue." +msgstr "" +"Du har ikke tillatelse til å sette denne prioriteten til denne hendelsen" + +#: taiga/projects/issues/api.py:172 +msgid "You don't have permissions to set this type to this issue." +msgstr "Du har ikke tillatelse til å sette denne typen til denne hendelsen." + +#: taiga/projects/issues/models.py:41 +msgid "severity" +msgstr "alvorlighetsgrad" + +#: taiga/projects/issues/models.py:43 +msgid "priority" +msgstr "prioritet" + +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 +#: taiga/projects/userstories/models.py:64 +msgid "milestone" +msgstr "milepæl" + +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +msgid "finished date" +msgstr "Sluttdato" + +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 +#: taiga/projects/userstories/models.py:108 +msgid "external reference" +msgstr "ekstern referanse" + +#: taiga/projects/likes/models.py:36 +msgid "Like" +msgstr "Liker" + +#: taiga/projects/likes/models.py:37 +msgid "Likes" +msgstr "Liker" + +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 +#: taiga/projects/models.py:513 taiga/projects/models.py:546 +#: taiga/projects/models.py:604 taiga/projects/models.py:678 +#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/users/admin.py:58 taiga/users/models.py:294 +msgid "slug" +msgstr "slug" + +#: taiga/projects/milestones/models.py:46 +msgid "estimated start date" +msgstr "anslått startdato" + +#: taiga/projects/milestones/models.py:47 +msgid "estimated finish date" +msgstr "anslått sluttdato" + +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 +#: taiga/projects/models.py:550 taiga/projects/models.py:608 +#: taiga/projects/models.py:682 +msgid "is closed" +msgstr "er lukket" + +#: taiga/projects/milestones/models.py:56 +msgid "disponibility" +msgstr "" + +#: taiga/projects/milestones/models.py:80 +msgid "The estimated start must be previous to the estimated finish." +msgstr "" + +#: taiga/projects/milestones/validators.py:33 +msgid "There's no milestone with that id" +msgstr "" + +#: taiga/projects/mixins/blocked.py:31 +msgid "is blocked" +msgstr "er blokkert" + +#: taiga/projects/mixins/ordering.py:49 +#, python-brace-format +msgid "'{param}' parameter is mandatory" +msgstr "'{param}' parameter er obligatorisk" + +#: taiga/projects/mixins/ordering.py:53 +msgid "'project' parameter is mandatory" +msgstr "'project' parameter er obligatorisk" + +#: taiga/projects/models.py:75 +msgid "email" +msgstr "epost" + +#: taiga/projects/models.py:77 +msgid "create at" +msgstr "opprett ved" + +#: taiga/projects/models.py:79 taiga/users/models.py:154 +msgid "token" +msgstr "token" + +#: taiga/projects/models.py:85 +msgid "invitation extra text" +msgstr "invitasjon ekstra tekst" + +#: taiga/projects/models.py:88 taiga/projects/models.py:734 +msgid "user order" +msgstr "bruker rekkefølge" + +#: taiga/projects/models.py:104 +msgid "The user is already member of the project" +msgstr "Denne brukeren er allerede medlem av prosjektet" + +#: taiga/projects/models.py:111 +msgid "default epic status" +msgstr "" + +#: taiga/projects/models.py:115 +msgid "default US status" +msgstr "standard brukerhistoriestatuser" + +#: taiga/projects/models.py:118 +msgid "default points" +msgstr "standardpoeng" + +#: taiga/projects/models.py:122 +msgid "default task status" +msgstr "standard oppgavestatuser" + +#: taiga/projects/models.py:125 +msgid "default priority" +msgstr "standard prioriteter" + +#: taiga/projects/models.py:128 +msgid "default severity" +msgstr "standard alvorlighetsgrad" + +#: taiga/projects/models.py:132 +msgid "default issue status" +msgstr "standard hendelsesstatuser" + +#: taiga/projects/models.py:136 +msgid "default issue type" +msgstr "standard hendelsestyper" + +#: taiga/projects/models.py:152 +msgid "logo" +msgstr "logo" + +#: taiga/projects/models.py:162 +msgid "members" +msgstr "medlemmer" + +#: taiga/projects/models.py:165 +msgid "total of milestones" +msgstr "total av milepæler" + +#: taiga/projects/models.py:166 +msgid "total story points" +msgstr "total historiepoeng" + +#: taiga/projects/models.py:169 taiga/projects/models.py:745 +msgid "active epics panel" +msgstr "" + +#: taiga/projects/models.py:171 taiga/projects/models.py:747 +msgid "active backlog panel" +msgstr "aktivt backlogpanel" + +#: taiga/projects/models.py:173 taiga/projects/models.py:749 +msgid "active kanban panel" +msgstr "aktivt kanbanpanel" + +#: taiga/projects/models.py:175 taiga/projects/models.py:751 +msgid "active wiki panel" +msgstr "aktivt wikipanel" + +#: taiga/projects/models.py:177 taiga/projects/models.py:753 +msgid "active issues panel" +msgstr "aktivt hendelsespanel" + +#: taiga/projects/models.py:180 taiga/projects/models.py:756 +msgid "videoconference system" +msgstr "videokonferansesystem" + +#: taiga/projects/models.py:182 taiga/projects/models.py:758 +msgid "videoconference extra data" +msgstr "videokonferanse ekstra data" + +#: taiga/projects/models.py:188 +msgid "creation template" +msgstr "skapelsesmal" + +#: taiga/projects/models.py:191 taiga/users/admin.py:62 +msgid "is private" +msgstr "er privat" + +#: taiga/projects/models.py:193 +msgid "anonymous permissions" +msgstr "anonymes rettigheter" + +#: taiga/projects/models.py:195 +msgid "user permissions" +msgstr "brukerrettigheter" + +#: taiga/projects/models.py:198 +msgid "is featured" +msgstr "er omtalt" + +#: taiga/projects/models.py:201 +msgid "is looking for people" +msgstr "er søker etter folk" + +#: taiga/projects/models.py:203 +msgid "loking for people note" +msgstr "søker etter folk notat" + +#: taiga/projects/models.py:217 +msgid "project transfer token" +msgstr "prosjektflyttingstoken" + +#: taiga/projects/models.py:221 +msgid "blocked code" +msgstr "blokkert kode" + +#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +msgid "updated date time" +msgstr "oppdatert dato tid" + +#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/votes/models.py:30 +msgid "count" +msgstr "antall" + +#: taiga/projects/models.py:231 +msgid "fans last week" +msgstr "fans forrige uke" + +#: taiga/projects/models.py:234 +msgid "fans last month" +msgstr "fans forrige måned" + +#: taiga/projects/models.py:237 +msgid "fans last year" +msgstr "fans forrige år" + +#: taiga/projects/models.py:243 +msgid "activity last week" +msgstr "aktivitet forrige uke" + +#: taiga/projects/models.py:246 +msgid "activity last month" +msgstr "aktivitet forrige måned" + +#: taiga/projects/models.py:249 +msgid "activity last year" +msgstr "aktivitet forrige år" + +#: taiga/projects/models.py:500 +msgid "modules config" +msgstr "modulkonfigurasjon" + +#: taiga/projects/models.py:552 +msgid "is archived" +msgstr "er arkivert" + +#: taiga/projects/models.py:556 +msgid "work in progress limit" +msgstr "arbeid som pågår grense" + +#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +msgid "value" +msgstr "verdi" + +#: taiga/projects/models.py:742 +msgid "default owner's role" +msgstr "standard eiers rolle" + +#: taiga/projects/models.py:760 +msgid "default options" +msgstr "standardvalg" + +#: taiga/projects/models.py:761 +msgid "epic statuses" +msgstr "" + +#: taiga/projects/models.py:762 +msgid "us statuses" +msgstr "bh statuser" + +#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 +#: taiga/projects/userstories/models.py:76 +msgid "points" +msgstr "poeng" + +#: taiga/projects/models.py:764 +msgid "task statuses" +msgstr "oppgavestatuser" + +#: taiga/projects/models.py:765 +msgid "issue statuses" +msgstr "hendelsesstatuser" + +#: taiga/projects/models.py:766 +msgid "issue types" +msgstr "hendelsestyper" + +#: taiga/projects/models.py:767 +msgid "priorities" +msgstr "prioriteter" + +#: taiga/projects/models.py:768 +msgid "severities" +msgstr "alvorlighetsgrader" + +#: taiga/projects/models.py:769 +msgid "roles" +msgstr "roller" + +#: taiga/projects/notifications/choices.py:30 +msgid "Involved" +msgstr "Involvert" + +#: taiga/projects/notifications/choices.py:31 +msgid "All" +msgstr "Alle" + +#: taiga/projects/notifications/choices.py:32 +msgid "None" +msgstr "Ingen" + +#: taiga/projects/notifications/models.py:64 +msgid "created date time" +msgstr "opprettet dato tid" + +#: taiga/projects/notifications/models.py:68 +msgid "history entries" +msgstr "loggoppføringer" + +#: taiga/projects/notifications/models.py:71 +msgid "notify users" +msgstr "varsle brukere" + +#: taiga/projects/notifications/models.py:93 +#: taiga/projects/notifications/models.py:94 +msgid "Watched" +msgstr "Fulgt" + +#: taiga/projects/notifications/services.py:65 +#: taiga/projects/notifications/services.py:79 +msgid "Notify exists for specified user and project" +msgstr "" + +#: taiga/projects/notifications/services.py:426 +msgid "Invalid value for notify level" +msgstr "Ugyldig verdi for varslingsnivå" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic updated

\n" +"

Hello %(user)s,
%(changer)s has updated a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Epic updated\n" +"Hello %(user)s, %(changer)s has updated a epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New epic created

\n" +"

Hello %(user)s,
%(changer)s has created a new epic on " +"%(project)s

\n" +"

Epic #%(ref)s %(subject)s

\n" +" See epic\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New epic created\n" +"Hello %(user)s, %(changer)s has created a new epic on %(project)s\n" +"See epic #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Epic deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a epic on %(project)s\n" +"

Epic #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Epic deleted\n" +"Hello %(user)s, %(changer)s has deleted a epic on %(project)s\n" +"Epic #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/epics/epic-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the epic #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue updated

\n" +"

Hello %(user)s,
%(changer)s has updated an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Issue updated\n" +"Hello %(user)s, %(changer)s has updated an issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New issue created

\n" +"

Hello %(user)s,
%(changer)s has created a new issue on " +"%(project)s

\n" +"

Issue #%(ref)s %(subject)s

\n" +" See issue\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New issue created\n" +"Hello %(user)s, %(changer)s has created a new issue on %(project)s\n" +"See issue #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Issue deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an issue on %(project)s\n" +"

Issue #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Issue deleted\n" +"Hello %(user)s, %(changer)s has deleted an issue on %(project)s\n" +"Issue #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/issues/issue-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the issue #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint updated

\n" +"

Hello %(user)s,
%(changer)s has updated an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See sprint\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Sprint updated\n" +"Hello %(user)s, %(changer)s has updated a sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the sprint \"%(milestone)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New sprint created

\n" +"

Hello %(user)s,
%(changer)s has created a new sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +" See " +"sprint\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New sprint created\n" +"Hello %(user)s, %(changer)s has created a new sprint on %(project)s\n" +"See sprint %(name)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the sprint \"%(milestone)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Sprint deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted an sprint on " +"%(project)s

\n" +"

Sprint %(name)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Sprint deleted\n" +"Hello %(user)s, %(changer)s has deleted an sprint on %(project)s\n" +"Sprint %(name)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/milestones/milestone-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the Sprint \"%(milestone)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task updated

\n" +"

Hello %(user)s,
%(changer)s has updated a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Task updated\n" +"Hello %(user)s, %(changer)s has updated a task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the task #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New task created

\n" +"

Hello %(user)s,
%(changer)s has created a new task on " +"%(project)s

\n" +"

Task #%(ref)s %(subject)s

\n" +" See task\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New task created\n" +"Hello %(user)s, %(changer)s has created a new task on %(project)s\n" +"See task #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the task #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Task deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a task on %(project)s\n" +"

Task #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Task deleted\n" +"Hello %(user)s, %(changer)s has deleted a task on %(project)s\n" +"Task #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/tasks/task-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the task #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story updated

\n" +"

Hello %(user)s,
%(changer)s has updated a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"User story updated\n" +"Hello %(user)s, %(changer)s has updated a user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-change-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Updated the US #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New user story created

\n" +"

Hello %(user)s,
%(changer)s has created a new user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +" See user story\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New user story created\n" +"Hello %(user)s, %(changer)s has created a new user story on %(project)s\n" +"See user story #%(ref)s %(subject)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-create-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Created the US #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

User Story deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a user story on " +"%(project)s

\n" +"

User Story #%(ref)s %(subject)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"User Story deleted\n" +"Hello %(user)s, %(changer)s has deleted a user story on %(project)s\n" +"User Story #%(ref)s %(subject)s\n" +"\n" +"---\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/userstories/userstory-delete-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Deleted the US #%(ref)s \"%(subject)s\"\n" +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki Page updated

\n" +"

Hello %(user)s,
%(changer)s has updated a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See Wiki Page\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-change-body-text.jinja:3 +#, python-format +msgid "" +"\n" +"Wiki Page updated\n" +"\n" +"Hello %(user)s, %(changer)s has updated a wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +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-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

New wiki page created

\n" +"

Hello %(user)s,
%(changer)s has created a new wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +" See " +"wiki page\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-create-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"New wiki page created\n" +"\n" +"Hello %(user)s, %(changer)s has created a new wiki page on %(project)s\n" +"\n" +"See wiki page %(page)s at %(url)s\n" +"\n" +"---\n" +"The Taiga Team\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-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Wiki page deleted

\n" +"

Hello %(user)s,
%(changer)s has deleted a wiki page on " +"%(project)s

\n" +"

Wiki page %(page)s

\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/notifications/templates/emails/wiki/wikipage-delete-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Wiki page deleted\n" +"\n" +"Hello %(user)s, %(changer)s has deleted a wiki page on %(project)s\n" +"\n" +"Wiki page %(page)s\n" +"\n" +"---\n" +"The Taiga Team\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/notifications/validators.py:48 +msgid "Watchers contains invalid users" +msgstr "Følgere inneholder ugyldige brukere" + +#: taiga/projects/occ/mixins.py:37 +msgid "The version must be an integer" +msgstr "Versjonen må være et heltall" + +#: taiga/projects/occ/mixins.py:60 +msgid "The version parameter is not valid" +msgstr "Versjonsparameteret er ikke gyldig" + +#: taiga/projects/occ/mixins.py:76 +msgid "The version doesn't match with the current one" +msgstr "Versjonen samsvarer ikke med den nåværende" + +#: taiga/projects/occ/mixins.py:95 +msgid "version" +msgstr "versjon" + +#: taiga/projects/permissions.py:44 +msgid "" +"You can't leave the project if you are the owner or there are no more admins" +msgstr "" +"Du kan ikke forlate prosjektet hvis du er eieren eller det ikke er flere " +"administratorer" + +#: taiga/projects/services/members.py:118 +msgid "Project without owner" +msgstr "" + +#: taiga/projects/services/members.py:123 +msgid "You have reached your current limit of memberships for private projects" +msgstr "Du har nådd din nåværende grense for medlemskap for private prosjekter" + +#: taiga/projects/services/members.py:127 +msgid "You have reached your current limit of memberships for public projects" +msgstr "" +"Du har nådd din nåværende grense for medlemskap for offentlige prosjekter" + +#: taiga/projects/services/projects.py:94 +#: taiga/projects/services/projects.py:134 taiga/users/services.py:589 +msgid "You can't have more private projects" +msgstr "Du kan ikke ha fler private prosjekter" + +#: taiga/projects/services/projects.py:98 +#: taiga/projects/services/projects.py:138 taiga/users/services.py:592 +msgid "" +"This project reaches your current limit of memberships for private projects" +msgstr "" +"Dette prosjektet kommer til å nå din nåværende grense for medlemskap for " +"private prosjekter" + +#: taiga/projects/services/projects.py:102 +#: taiga/projects/services/projects.py:142 taiga/users/services.py:596 +msgid "You can't have more public projects" +msgstr "Du kan ikke ha flere offentlige prosjekter" + +#: taiga/projects/services/projects.py:106 +#: taiga/projects/services/projects.py:146 taiga/users/services.py:599 +msgid "" +"This project reaches your current limit of memberships for public projects" +msgstr "" +"Dette prosjektet kommer til å nå din nåværende grense for medlemskap for " +"offentlige prosjekter" + +#: taiga/projects/services/stats.py:197 +msgid "Future sprint" +msgstr "Fremtidig sprint" + +#: taiga/projects/services/stats.py:217 +msgid "Project End" +msgstr "Prosjektslutt" + +#: taiga/projects/services/transfer.py:62 +#: taiga/projects/services/transfer.py:69 +#: taiga/projects/services/transfer.py:72 taiga/users/api.py:186 +#: taiga/users/api.py:191 +msgid "Token is invalid" +msgstr "Token er ugyldig" + +#: taiga/projects/services/transfer.py:67 +msgid "Token has expired" +msgstr "Token er utløpt" + +#: taiga/projects/tagging/fields.py:52 +#, python-brace-format +msgid "Invalid tag '{value}'. The color is not a valid HEX color or null." +msgstr "" + +#: taiga/projects/tagging/fields.py:55 +#, python-brace-format +msgid "" +"Invalid tag '{value}'. it must be the name or a pair '[\"name\", \"hex color/" +"\" | null]'." +msgstr "" + +#: taiga/projects/tagging/fields.py:77 +#, python-brace-format +msgid "Invalid tag '{value}'. It must be the tag name." +msgstr "" + +#: taiga/projects/tagging/models.py:27 +msgid "tags" +msgstr "etiketter" + +#: taiga/projects/tagging/models.py:35 +msgid "tags colors" +msgstr "etiketter farge" + +#: taiga/projects/tagging/validators.py:47 +#: taiga/projects/tagging/validators.py:74 +msgid "This tag already exists." +msgstr "" + +#: taiga/projects/tagging/validators.py:54 +#: taiga/projects/tagging/validators.py:81 +msgid "The color is not a valid HEX color." +msgstr "" + +#: taiga/projects/tagging/validators.py:67 +#: taiga/projects/tagging/validators.py:92 +#: taiga/projects/tagging/validators.py:105 +#: taiga/projects/tagging/validators.py:112 +msgid "The tag doesn't exist." +msgstr "" + +#: taiga/projects/tasks/api.py:97 taiga/projects/tasks/api.py:106 +msgid "You don't have permissions to set this sprint to this task." +msgstr "Du har ikke tillatelse til å sette denne sprinten til denne oppgaven." + +#: taiga/projects/tasks/api.py:100 +msgid "You don't have permissions to set this user story to this task." +msgstr "" +"Du har ikke tillatelse til å sette denne brukerhistorien til denne oppgaven." + +#: taiga/projects/tasks/api.py:103 +msgid "You don't have permissions to set this status to this task." +msgstr "Du har ikke tillatelse til å sette denne statusen til denne oppgaven." + +#: taiga/projects/tasks/models.py:57 +msgid "us order" +msgstr "BH rekkefølge" + +#: taiga/projects/tasks/models.py:59 +msgid "taskboard order" +msgstr "Oppgavetavle rekkefølge" + +#: taiga/projects/tasks/models.py:67 +msgid "is iocaine" +msgstr "Er Iocaine" + +#: taiga/projects/tasks/validators.py:59 +msgid "Invalid milestone id." +msgstr "" + +#: taiga/projects/tasks/validators.py:70 +msgid "Invalid task status id." +msgstr "" + +#: taiga/projects/tasks/validators.py:83 +msgid "Invalid user story id." +msgstr "" + +#: taiga/projects/tasks/validators.py:107 +msgid "Invalid task status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:121 +msgid "Invalid user story id. The user story must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:133 +msgid "Invalid milestone id. The milestone must belong to the same project." +msgstr "" + +#: taiga/projects/tasks/validators.py:150 +msgid "" +"Invalid task ids. All tasks must belong to the same project and, if it " +"exists, to the same status, user story and/or milestone." +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:6 +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:4 +msgid "someone" +msgstr "noen" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:11 +#, python-format +msgid "" +"\n" +"

You have been invited to Taiga!

\n" +"

Hi! %(full_name)s has sent you an invitation to join project " +"%(project)s in Taiga.
Taiga is a Free, open Source Agile Project " +"Management Tool.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:17 +#, 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:24 +msgid "Accept your invitation to Taiga" +msgstr "Godta invitasjonen til Taiga" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:24 +msgid "Accept your invitation" +msgstr "Godta din invitasjon" + +#: taiga/projects/templates/emails/membership_invitation-body-html.jinja:25 +msgid "The Taiga Team" +msgstr "Taiga Teamet" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:6 +#, python-format +msgid "" +"\n" +"You, or someone you know, has invited you to Taiga\n" +"\n" +"Hi! %(full_name)s has sent you an invitation to join a project called " +"%(project)s which is being managed on Taiga, a Free, open Source Agile " +"Project Management Tool.\n" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:12 +#, 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:18 +msgid "Accept your invitation to Taiga following this link:" +msgstr "" + +#: taiga/projects/templates/emails/membership_invitation-body-text.jinja:20 +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-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

You have been added to a project

\n" +"

Hello %(full_name)s,
you have been added to the project " +"%(project)s

\n" +" Go to " +"project\n" +"

The Taiga Team

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/membership_notification-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"You have been added to a project\n" +"Hello %(full_name)s,you have been added to the project %(project)s\n" +"\n" +"See project at %(url)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/templates/emails/transfer_accept-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(old_owner_name)s,

\n" +"

%(new_owner_name)s has accepted your offer and will become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:10 +#, python-format +msgid "

%(new_owner_name)s says:

" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14 +msgid "" +"\n" +"

From now on, your new status for this project will be \"admin\".\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(old_owner_name)s,\n" +"%(new_owner_name)s has accepted your offer and will become the new project " +"owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7 +#, python-format +msgid "%(new_owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:11 +msgid "" +"\n" +"From now on, your new status for this project will be \"admin\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:16 +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:19 +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:13 +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:18 +msgid "" +"\n" +"The Taiga Team\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_accept-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer accepted!\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(rejecter_name)s has declined your offer and will not become the " +"new project owner for \"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(rejecter_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:16 +msgid "" +"\n" +"

If you want, you can still try to transfer the project ownership to a " +"different person.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:21 +#: taiga/projects/templates/emails/transfer_reject-body-html.jinja:22 +msgid "Request transfer to a different person" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(rejecter_name)s has declined your offer and will not become the new " +"project owner for \"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:7 +#, python-format +msgid "%(rejecter_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:11 +msgid "" +"\n" +"If you want, you can still try to transfer the project ownership to a " +"different person.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-body-text.jinja:15 +msgid "Request transfer to a different person:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_reject-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer declined\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(owner_name)s,

\n" +"

%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:9 +msgid "" +"\n" +"

Please, click on \"Continue\" if you would like to start the " +"project transfer from the administration panel.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-html.jinja:14 +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:22 +msgid "Continue" +msgstr "Fortsett" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(owner_name)s,\n" +"%(requester_name)s has requested to become the project owner for " +"\"%(project_name)s\".\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:6 +msgid "" +"\n" +"Please, go to your project settings if you would like to start the project " +"transfer from the administration panel.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-body-text.jinja:10 +msgid "Go to your project settings:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_request-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer request\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Hi %(receiver_name)s,

\n" +"

%(owner_name)s, the current project owner at \"%(project_name)s\" " +"would like you to become the new project owner.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:10 +#, python-format +msgid "" +"\n" +"

%(owner_name)s says:

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-html.jinja:17 +msgid "" +"\n" +"

Please, click on \"Continue\" to either accept or reject this " +"proposal.

\n" +" " +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:1 +#, python-format +msgid "" +"\n" +"Hi %(receiver_name)s,\n" +"%(owner_name)s, the current project owner at \"%(project_name)s\" would like " +"you to become the new project owner.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:6 +#, python-format +msgid "%(owner_name)s says:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:11 +msgid "" +"\n" +"Please, go to the following link to either accept or reject this proposal.\n" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-body-text.jinja:15 +msgid "Accept or reject the project ownership transfer:" +msgstr "" + +#: taiga/projects/templates/emails/transfer_start-subject.jinja:1 +#, python-format +msgid "" +"\n" +"[%(project)s] Project ownership transfer offer\n" +msgstr "" + +#. Translators: Name of scrum project template. +#: taiga/projects/translations.py:30 +msgid "Scrum" +msgstr "Scrum" + +#. Translators: Description of scrum project template. +#: taiga/projects/translations.py:32 +msgid "" +"The agile product backlog in Scrum is a prioritized features list, " +"containing short descriptions of all functionality desired in the product. " +"When applying Scrum, it's not necessary to start a project with a lengthy, " +"upfront effort to document all requirements. The Scrum product backlog is " +"then allowed to grow and change as more is learned about the product and its " +"customers" +msgstr "" + +#. Translators: Name of kanban project template. +#: taiga/projects/translations.py:35 +msgid "Kanban" +msgstr "Kanban" + +#. Translators: Description of kanban project template. +#: taiga/projects/translations.py:37 +msgid "" +"Kanban is a method for managing knowledge work with an emphasis on just-in-" +"time delivery while not overloading the team members. In this approach, the " +"process, from definition of a task to its delivery to the customer, is " +"displayed for participants to see and team members pull work from a queue." +msgstr "" + +#. Translators: User story point value (value = undefined) +#: taiga/projects/translations.py:45 +msgid "?" +msgstr "?" + +#. Translators: User story point value (value = 0) +#: taiga/projects/translations.py:47 +msgid "0" +msgstr "0" + +#. Translators: User story point value (value = 0.5) +#: taiga/projects/translations.py:49 +msgid "1/2" +msgstr "1/2" + +#. Translators: User story point value (value = 1) +#: taiga/projects/translations.py:51 +msgid "1" +msgstr "1" + +#. Translators: User story point value (value = 2) +#: taiga/projects/translations.py:53 +msgid "2" +msgstr "2" + +#. Translators: User story point value (value = 3) +#: taiga/projects/translations.py:55 +msgid "3" +msgstr "3" + +#. Translators: User story point value (value = 5) +#: taiga/projects/translations.py:57 +msgid "5" +msgstr "5" + +#. Translators: User story point value (value = 8) +#: taiga/projects/translations.py:59 +msgid "8" +msgstr "8" + +#. Translators: User story point value (value = 10) +#: taiga/projects/translations.py:61 +msgid "10" +msgstr "10" + +#. Translators: User story point value (value = 13) +#: taiga/projects/translations.py:63 +msgid "13" +msgstr "13" + +#. Translators: User story point value (value = 20) +#: taiga/projects/translations.py:65 +msgid "20" +msgstr "20" + +#. Translators: User story point value (value = 40) +#: taiga/projects/translations.py:67 +msgid "40" +msgstr "40" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:75 taiga/projects/translations.py:98 +#: taiga/projects/translations.py:114 +msgid "New" +msgstr "Ny" + +#. Translators: User story status +#: taiga/projects/translations.py:78 +msgid "Ready" +msgstr "Klar" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:81 taiga/projects/translations.py:100 +#: taiga/projects/translations.py:116 +msgid "In progress" +msgstr "Under arbeid" + +#. Translators: User story status +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:84 taiga/projects/translations.py:102 +#: taiga/projects/translations.py:118 +msgid "Ready for test" +msgstr "Klar til test" + +#. Translators: User story status +#: taiga/projects/translations.py:87 +msgid "Done" +msgstr "Ferdig" + +#. Translators: User story status +#: taiga/projects/translations.py:90 +msgid "Archived" +msgstr "Arkivert" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:104 taiga/projects/translations.py:120 +msgid "Closed" +msgstr "Lukket" + +#. Translators: Task status +#. Translators: Issue status +#: taiga/projects/translations.py:106 taiga/projects/translations.py:122 +msgid "Needs Info" +msgstr "Trenger info" + +#. Translators: Issue status +#: taiga/projects/translations.py:124 +msgid "Postponed" +msgstr "Utsatt" + +#. Translators: Issue status +#: taiga/projects/translations.py:126 +msgid "Rejected" +msgstr "Avslått" + +#. Translators: Issue type +#: taiga/projects/translations.py:134 +msgid "Bug" +msgstr "Bug" + +#. Translators: Issue type +#: taiga/projects/translations.py:136 +msgid "Question" +msgstr "Spørsmål" + +#. Translators: Issue type +#: taiga/projects/translations.py:138 +msgid "Enhancement" +msgstr "Forbedring" + +#. Translators: Issue priority +#: taiga/projects/translations.py:146 +msgid "Low" +msgstr "Lav" + +#. Translators: Issue priority +#. Translators: Issue severity +#: taiga/projects/translations.py:148 taiga/projects/translations.py:161 +msgid "Normal" +msgstr "Normal" + +#. Translators: Issue priority +#: taiga/projects/translations.py:150 +msgid "High" +msgstr "Høy" + +#. Translators: Issue severity +#: taiga/projects/translations.py:157 +msgid "Wishlist" +msgstr "Ønskeliste" + +#. Translators: Issue severity +#: taiga/projects/translations.py:159 +msgid "Minor" +msgstr "Liten" + +#. Translators: Issue severity +#: taiga/projects/translations.py:163 +msgid "Important" +msgstr "Viktig" + +#. Translators: Issue severity +#: taiga/projects/translations.py:165 +msgid "Critical" +msgstr "Kritisk" + +#. Translators: User role +#: taiga/projects/translations.py:172 +msgid "UX" +msgstr "UX" + +#. Translators: User role +#: taiga/projects/translations.py:174 +msgid "Design" +msgstr "Design" + +#. Translators: User role +#: taiga/projects/translations.py:176 +msgid "Front" +msgstr "Front" + +#. Translators: User role +#: taiga/projects/translations.py:178 +msgid "Back" +msgstr "Back" + +#. Translators: User role +#: taiga/projects/translations.py:180 +msgid "Product Owner" +msgstr "Produkteier" + +#. Translators: User role +#: taiga/projects/translations.py:182 +msgid "Stakeholder" +msgstr "Interessent" + +#: taiga/projects/userstories/api.py:119 +msgid "You don't have permissions to set this sprint to this user story." +msgstr "" +"Du har ikke tillatelse til å sette denne sprinten til denne brukerhistorien." + +#: taiga/projects/userstories/api.py:123 +msgid "You don't have permissions to set this status to this user story." +msgstr "" +"Du har ikke tillatelse til å sette denne statusen til denne brukerhistorien." + +#: taiga/projects/userstories/api.py:213 +#, python-brace-format +msgid "Invalid role id '{role_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:220 +#, python-brace-format +msgid "Invalid points id '{points_id}'" +msgstr "" + +#: taiga/projects/userstories/api.py:235 +#, python-brace-format +msgid "Generating the user story #{ref} - {subject}" +msgstr "Genererer brukerhistorien #{ref} - {subject}" + +#: taiga/projects/userstories/api.py:296 +msgid "ref param is needed" +msgstr "" + +#: taiga/projects/userstories/api.py:299 +msgid "project or project_slug param is needed" +msgstr "" + +#: taiga/projects/userstories/models.py:40 +msgid "role" +msgstr "rolle" + +#: taiga/projects/userstories/models.py:79 +msgid "backlog order" +msgstr "backlog rekkefølge" + +#: taiga/projects/userstories/models.py:81 +msgid "sprint order" +msgstr "sprint rekkefølge" + +#: taiga/projects/userstories/models.py:83 +msgid "kanban order" +msgstr "" + +#: taiga/projects/userstories/models.py:91 +msgid "finish date" +msgstr "Sluttdato" + +#: taiga/projects/userstories/models.py:106 +msgid "generated from issue" +msgstr "" + +#: taiga/projects/userstories/validators.py:43 +msgid "There's no user story with that id" +msgstr "Det finnes ingen brukerhistorie med den id'en" + +#: taiga/projects/userstories/validators.py:82 +#: taiga/projects/userstories/validators.py:108 +msgid "" +"Invalid user story status id. The status must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:120 +msgid "Invalid milestone id. The milistone must belong to the same project." +msgstr "" + +#: taiga/projects/userstories/validators.py:135 +msgid "" +"Invalid user story ids. All stories must belong to the same project and, if " +"it exists, to the same status and milestone." +msgstr "" + +#: taiga/projects/userstories/validators.py:159 +msgid "The milestone isn't valid for the project" +msgstr "" + +#: taiga/projects/userstories/validators.py:169 +msgid "All the user stories must be from the same project" +msgstr "" + +#: taiga/projects/validators.py:61 +msgid "There's no project with that id" +msgstr "Det finnes ikke noe prosjekt med den id'en" + +#: taiga/projects/validators.py:142 +msgid "Email address is already taken" +msgstr "E-postadressen er allerede tatt" + +#: taiga/projects/validators.py:154 +msgid "Invalid role for the project" +msgstr "Ugyldig rolle for prosjektet" + +#: taiga/projects/validators.py:165 +msgid "The project owner must be admin." +msgstr "Prosjekteieren skal være admin." + +#: taiga/projects/validators.py:169 +msgid "At least one user must be an active admin for this project." +msgstr "Minst en bruker må være en aktiv administrator for dette prosjektet." + +#: taiga/projects/validators.py:201 +msgid "Invalid role ids. All roles must belong to the same project." +msgstr "" + +#: taiga/projects/validators.py:225 +msgid "Default options" +msgstr "Standardvalgene" + +#: taiga/projects/validators.py:226 +msgid "User story's statuses" +msgstr "Brukerhistoriestatuser" + +#: taiga/projects/validators.py:227 +msgid "Points" +msgstr "Poeng" + +#: taiga/projects/validators.py:228 +msgid "Task's statuses" +msgstr "Oppgavestatuser" + +#: taiga/projects/validators.py:229 +msgid "Issue's statuses" +msgstr "Hendelsesstatuser" + +#: taiga/projects/validators.py:230 +msgid "Issue's types" +msgstr "Hendelsestyper" + +#: taiga/projects/validators.py:231 +msgid "Priorities" +msgstr "Prioriteter" + +#: taiga/projects/validators.py:232 +msgid "Severities" +msgstr "Alvorlighetsgrad" + +#: taiga/projects/validators.py:233 +msgid "Roles" +msgstr "Roller" + +#: taiga/projects/votes/models.py:33 taiga/projects/votes/models.py:34 +#: taiga/projects/votes/models.py:58 +msgid "Votes" +msgstr "Stemmer" + +#: taiga/projects/votes/models.py:57 +msgid "Vote" +msgstr "Stemme" + +#: taiga/projects/wiki/api.py:77 +msgid "'content' parameter is mandatory" +msgstr "'content' parameteren er obligatorisk" + +#: taiga/projects/wiki/api.py:80 +msgid "'project_id' parameter is mandatory" +msgstr "'project_id' parameteren er obligatorisk" + +#: taiga/projects/wiki/models.py:41 +msgid "last modifier" +msgstr "sist endret av" + +#: taiga/projects/wiki/models.py:74 +msgid "href" +msgstr "href" + +#: taiga/timeline/signals.py:63 +msgid "Check the history API for the exact diff" +msgstr "Sjekk historieAPI'et for den eksakte forskjellen" + +#: taiga/users/admin.py:39 +msgid "Project Member" +msgstr "Prosjektmedlem" + +#: taiga/users/admin.py:40 +msgid "Project Members" +msgstr "Prosjektmedlemmer" + +#: taiga/users/admin.py:50 +msgid "id" +msgstr "id" + +#: taiga/users/admin.py:81 +msgid "Project Ownership" +msgstr "Prosjekteierskap" + +#: taiga/users/admin.py:82 +msgid "Project Ownerships" +msgstr "Prosjekteierskap" + +#: taiga/users/admin.py:119 +msgid "Personal info" +msgstr "Personlig informasjon" + +#: taiga/users/admin.py:122 +msgid "Permissions" +msgstr "Tilganger" + +#: taiga/users/admin.py:123 +msgid "Restrictions" +msgstr "Restriksjoner" + +#: taiga/users/admin.py:125 +msgid "Important dates" +msgstr "Viktige datoer" + +#: taiga/users/api.py:123 +msgid "Duplicated email" +msgstr "Duplikat e-post" + +#: taiga/users/api.py:125 +msgid "Not valid email" +msgstr "Ikke gyldig epost" + +#: taiga/users/api.py:165 +msgid "Invalid username or email" +msgstr "Ugyldig brukernavn eller epost" + +#: taiga/users/api.py:174 +msgid "Mail sended successful!" +msgstr "Epost sendt!" + +#: taiga/users/api.py:212 +msgid "Current password parameter needed" +msgstr "Nåværende passord er nødvendig" + +#: taiga/users/api.py:215 +msgid "New password parameter needed" +msgstr "Nytt passord er nødvendig" + +#: taiga/users/api.py:218 +msgid "Invalid password length at least 6 charaters needed" +msgstr "Ugyldig lengde på passord. Minst 6 tegn" + +#: taiga/users/api.py:221 +msgid "Invalid current password" +msgstr "Ugyldig nåværende passord" + +#: taiga/users/api.py:268 taiga/users/api.py:274 +msgid "" +"Invalid, are you sure the token is correct and you didn't use it before?" +msgstr "" +"Ugyldig, er du sikker på at token er korrekt og at du ikke har brukt den før?" + +#: taiga/users/api.py:301 taiga/users/api.py:309 taiga/users/api.py:312 +msgid "Invalid, are you sure the token is correct?" +msgstr "Ugyldig, er du sikker på at token er korrekt?" + +#: taiga/users/models.py:95 +msgid "superuser status" +msgstr "superbrukerstatus" + +#: taiga/users/models.py:96 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "" +"Angir at denne brukeren har alle tillatelser uten eksplisitt tildele dem." + +#: taiga/users/models.py:126 +msgid "username" +msgstr "brukernavn" + +#: taiga/users/models.py:127 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and /./-/_ characters" +msgstr "Påkrevd. 30 tegn eller færre. Bokstaver, tall og /./-/_ tegn" + +#: taiga/users/models.py:130 +msgid "Enter a valid username." +msgstr "Skriv inn et gyldig brukernavn" + +#: taiga/users/models.py:133 +msgid "active" +msgstr "aktiv" + +#: taiga/users/models.py:134 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "" +"Betegner om denne brukeren bør behandles som aktiv. Velg bort dette i stedet " +"for å slette kontoer." + +#: taiga/users/models.py:140 +msgid "biography" +msgstr "biografi" + +#: taiga/users/models.py:143 +msgid "photo" +msgstr "bilde" + +#: taiga/users/models.py:144 +msgid "date joined" +msgstr "dato ble med" + +#: taiga/users/models.py:146 +msgid "default language" +msgstr "standardspråk" + +#: taiga/users/models.py:148 +msgid "default theme" +msgstr "standard tema" + +#: taiga/users/models.py:150 +msgid "default timezone" +msgstr "standard tidssone" + +#: taiga/users/models.py:152 +msgid "colorize tags" +msgstr "fargelegg etiketter" + +#: taiga/users/models.py:157 +msgid "email token" +msgstr "epost token" + +#: taiga/users/models.py:159 +msgid "new email address" +msgstr "ny epostadresse" + +#: taiga/users/models.py:166 +msgid "max number of owned private projects" +msgstr "maks antall eide private prosjekter" + +#: taiga/users/models.py:169 +msgid "max number of owned public projects" +msgstr "maks antall eide offentlige prosjekter" + +#: taiga/users/models.py:172 +msgid "max number of memberships for each owned private project" +msgstr "maks antall medlemskap for hvert eide private prosjekt" + +#: taiga/users/models.py:176 +msgid "max number of memberships for each owned public project" +msgstr "maks antall medlemskap for hvetr eide offentlige prosjekt" + +#: taiga/users/models.py:296 +msgid "permissions" +msgstr "rettigheter" + +#: taiga/users/services.py:51 taiga/users/services.py:68 +msgid "Username or password does not matches user." +msgstr "Brukernavn eller passord passer ikke til brukeren." + +#: 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 "" +"\n" +"

Endre din epost

\n" +"

Hallo %(full_name)s,
vennligst bekreft din epost

\n" +" Bekreft " +"epost\n" +"

Du kan ignorere denne meldingen dersom du ikke bestilte endringen.\n" +"

Taiga Teamet

\n" +" " + +#: 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 "" +"\n" +"Hallo %(full_name)s, vennligst bekreft din epost\n" +"\n" +"%(url)s\n" +"\n" +"Du kan ignorere denne meldingen dersom du ikke bestilte endringen\n" +"\n" +"---\n" +"Taiga Teamet\n" + +#: taiga/users/templates/emails/change_email-subject.jinja:1 +msgid "[Taiga] Change email" +msgstr "[Taiga] Endre epost" + +#: taiga/users/templates/emails/password_recovery-body-html.jinja:4 +#, python-format +msgid "" +"\n" +"

Recover your password

\n" +"

Hello %(full_name)s,
you asked to recover your password

\n" +" Recover your password\n" +"

You can ignore this message if you did not request.

\n" +"

The Taiga Team

\n" +" " +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" +" \n" +" " +msgstr "" + +#: taiga/users/templates/emails/registered_user-body-html.jinja:23 +#, python-format +msgid "" +"\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 "" +"\n" +"Du kan fjerne din konto fra denne tjenesten: %(url)s\n" + +#: taiga/users/templates/emails/registered_user-subject.jinja:1 +msgid "You've been Taigatized!" +msgstr "Du har blitt Taigatisert!" + +#: taiga/users/validators.py:45 +msgid "invalid" +msgstr "ugyldig" + +#: taiga/users/validators.py:56 +msgid "Invalid username. Try with a different one." +msgstr "Ugyldig brukernavn. Prøv med et annet et." + +#: taiga/userstorage/api.py:53 +msgid "" +"Duplicate key value violates unique constraint. Key '{}' already exists." +msgstr "" +"Duplicate nøkkelverdi bryter unik begrensning. Nøkkelen \"{}\" finnes " +"allerede." + +#: taiga/userstorage/models.py:32 +msgid "key" +msgstr "nøkkel" + +#: taiga/webhooks/models.py:30 taiga/webhooks/models.py:40 +msgid "URL" +msgstr "URL" + +#: taiga/webhooks/models.py:31 +msgid "secret key" +msgstr "hemmelig nøkkel" + +#: taiga/webhooks/models.py:41 +msgid "status code" +msgstr "statuskode" + +#: taiga/webhooks/models.py:42 +msgid "request data" +msgstr "forespørselsdata" + +#: taiga/webhooks/models.py:43 +msgid "request headers" +msgstr "" + +#: taiga/webhooks/models.py:44 +msgid "response data" +msgstr "" + +#: taiga/webhooks/models.py:45 +msgid "response headers" +msgstr "" + +#: taiga/webhooks/models.py:46 +msgid "duration" +msgstr "varighet" From 9a06a8df94e947ec6fd9d6b30e3ed90f423c45f7 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 21 Sep 2016 13:21:14 +0200 Subject: [PATCH 245/261] Fixing tags actions with epics --- taiga/projects/tagging/services.py | 84 ++++++++------ taiga/projects/tagging/validators.py | 9 ++ tests/factories.py | 20 ++++ tests/integration/test_epics_tags.py | 161 +++++++++++++++++++++++++++ tests/integration/test_projects.py | 16 +++ 5 files changed, 253 insertions(+), 37 deletions(-) create mode 100644 tests/integration/test_epics_tags.py diff --git a/taiga/projects/tagging/services.py b/taiga/projects/tagging/services.py index 30e9f9dc..43cf8567 100644 --- a/taiga/projects/tagging/services.py +++ b/taiga/projects/tagging/services.py @@ -24,7 +24,7 @@ def tag_exist_for_project_elements(project, tag): def create_tags(project, new_tags_colors): - project.tags_colors += [[k, v] for k,v in new_tags_colors.items()] + project.tags_colors += [[k, v] for k, v in new_tags_colors.items()] project.save(update_fields=["tags_colors"]) @@ -33,41 +33,8 @@ def create_tag(project, tag, color): project.save(update_fields=["tags_colors"]) -def edit_tag(project, from_tag, to_tag=None, color=None): - tags_colors = dict(project.tags_colors) - - if color is not None: - tags_colors = dict(project.tags_colors) - tags_colors[from_tag] = color - - if to_tag is not None: - color = dict(project.tags_colors)[from_tag] - sql = """ - UPDATE userstories_userstory - SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) - WHERE project_id = {project_id}; - - UPDATE tasks_task - SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) - WHERE project_id = {project_id}; - - UPDATE issues_issue - SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) - WHERE project_id = {project_id}; - """ - sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag) - cursor = connection.cursor() - cursor.execute(sql) - - tags_colors[to_tag] = tags_colors.pop(from_tag) - - - project.tags_colors = list(tags_colors.items()) - project.save(update_fields=["tags_colors"]) - - -def rename_tag(project, from_tag, to_tag, color=None): - color = color or dict(project.tags_colors)[from_tag] +def edit_tag(project, from_tag, to_tag, color): + print("edit_tag", project, from_tag, to_tag, color) sql = """ UPDATE userstories_userstory SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) @@ -80,6 +47,45 @@ def rename_tag(project, from_tag, to_tag, color=None): UPDATE issues_issue SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) WHERE project_id = {project_id}; + + UPDATE epics_epic + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + """ + sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag) + cursor = connection.cursor() + cursor.execute(sql) + + tags_colors = dict(project.tags_colors) + tags_colors.pop(from_tag) + tags_colors[to_tag] = color + project.tags_colors = list(tags_colors.items()) + project.save(update_fields=["tags_colors"]) + + +def rename_tag(project, from_tag, to_tag, **kwargs): + # Kwargs can have a color parameter + update_color = "color" in kwargs + if update_color: + color = kwargs.get("color") + else: + color = dict(project.tags_colors)[from_tag] + sql = """ + UPDATE userstories_userstory + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE tasks_task + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE issues_issue + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; + + UPDATE epics_epic + SET tags = array_distinct(array_replace(tags, '{from_tag}', '{to_tag}')) + WHERE project_id = {project_id}; """ sql = sql.format(project_id=project.id, from_tag=from_tag, to_tag=to_tag, color=color) cursor = connection.cursor() @@ -105,6 +111,10 @@ def delete_tag(project, tag): UPDATE issues_issue SET tags = array_remove(tags, '{tag}') WHERE project_id = {project_id}; + + UPDATE epics_epic + SET tags = array_remove(tags, '{tag}') + WHERE project_id = {project_id}; """ sql = sql.format(project_id=project.id, tag=tag) cursor = connection.cursor() @@ -119,4 +129,4 @@ def delete_tag(project, tag): def mix_tags(project, from_tags, to_tag): color = dict(project.tags_colors)[to_tag] for from_tag in from_tags: - rename_tag(project, from_tag, to_tag, color) + rename_tag(project, from_tag, to_tag, color=color) diff --git a/taiga/projects/tagging/validators.py b/taiga/projects/tagging/validators.py index 779c247c..ea0c32c8 100644 --- a/taiga/projects/tagging/validators.py +++ b/taiga/projects/tagging/validators.py @@ -82,6 +82,15 @@ class EditTagTagValidator(ProjectTagValidator): return attrs + def validate(self, data): + if "to_tag" not in data: + data["to_tag"] = data.get("from_tag") + + if "color" not in data: + data["color"] = dict(self.project.tags_colors).get(data.get("from_tag")) + + return data + class DeleteTagValidator(ProjectTagValidator): tag = serializers.CharField() diff --git a/tests/factories.py b/tests/factories.py index 5cec5800..50f35122 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -670,6 +670,26 @@ def create_userstory(**kwargs): return UserStoryFactory(**defaults) +def create_epic(**kwargs): + "Create an epic along with its dependencies" + + owner = kwargs.pop("owner", None) + if not owner: + owner = UserFactory.create() + + project = kwargs.pop("project", None) + if project is None: + project = ProjectFactory.create(owner=owner) + + defaults = { + "project": project, + "owner": owner, + } + defaults.update(kwargs) + + return EpicFactory(**defaults) + + def create_project(**kwargs): "Create a project along with its dependencies" defaults = {} diff --git a/tests/integration/test_epics_tags.py b/tests/integration/test_epics_tags.py new file mode 100644 index 00000000..3e2c66c8 --- /dev/null +++ b/tests/integration/test_epics_tags.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 unittest import mock +from collections import OrderedDict + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_api_epic_add_new_tags_with_error(client): + project = f.ProjectFactory.create() + epic = f.create_epic(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=epic.owner, is_admin=True) + url = reverse("epics-detail", kwargs={"pk": epic.pk}) + data = { + "tags": [], + "version": epic.version + } + + client.login(epic.owner) + + data["tags"] = [1] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [["back", "#cccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + data["tags"] = [[1, "#ccc"]] + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 400, response.data + assert "tags" in response.data + + +def test_api_epic_add_new_tags_without_colors(client): + project = f.ProjectFactory.create() + epic = f.create_epic(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=epic.owner, is_admin=True) + url = reverse("epics-detail", kwargs={"pk": epic.pk}) + data = { + "tags": [ + ["back", None], + ["front", None], + ["ux", None] + ], + "version": epic.version + } + + client.login(epic.owner) + + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + + +def test_api_epic_add_new_tags_with_colors(client): + project = f.ProjectFactory.create() + epic = f.create_epic(project=project, status__project=project) + f.MembershipFactory.create(project=project, user=epic.owner, is_admin=True) + url = reverse("epics-detail", kwargs={"pk": epic.pk}) + data = { + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ], + "version": epic.version + } + + client.login(epic.owner) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + tags_colors = OrderedDict(project.tags_colors) + assert not tags_colors.keys() + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert "back" in tags_colors and "front" in tags_colors and "ux" in tags_colors + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + + +def test_api_create_new_epic_with_tags(client): + project = f.ProjectFactory.create(tags_colors=[["front", "#aaaaaa"], ["ux", "#fabada"]]) + status = f.EpicStatusFactory.create(project=project) + project.default_epic_status = status + project.save() + f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) + url = reverse("epics-list") + + data = { + "subject": "Test user story", + "project": project.id, + "tags": [ + ["back", "#fff8e7"], + ["front", None], + ["ux", "#fabada"] + ] + } + + client.login(project.owner) + + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201, response.data + + epic_tags_colors = OrderedDict(response.data["tags"]) + + assert epic_tags_colors["back"] == "#fff8e7" + assert epic_tags_colors["front"] == "#aaaaaa" + assert epic_tags_colors["ux"] == "#fabada" + + tags_colors = OrderedDict(project.tags_colors) + + project.refresh_from_db() + + tags_colors = OrderedDict(project.tags_colors) + assert tags_colors["back"] == "#fff8e7" + assert tags_colors["ux"] == "#fabada" + assert tags_colors["front"] == "#aaaaaa" diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index fbcc5e1e..a002aa0f 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -31,6 +31,7 @@ from taiga.projects.models import Project from taiga.projects.userstories.models import UserStory from taiga.projects.tasks.models import Task from taiga.projects.issues.models import Issue +from taiga.projects.epics.models import Epic from taiga.projects.choices import BLOCKED_BY_DELETING from .. import factories as f @@ -1918,6 +1919,7 @@ def test_edit_tag_only_name(client, settings): user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) task = f.TaskFactory.create(project=project, tags=["tag"]) issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) role = f.RoleFactory.create(project=project, permissions=["view_project"]) membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) @@ -1940,6 +1942,8 @@ def test_edit_tag_only_name(client, settings): assert task.tags == ["renamed_tag"] issue = Issue.objects.get(id=issue.pk) assert issue.tags == ["renamed_tag"] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == ["renamed_tag"] def test_edit_tag_only_color(client, settings): @@ -1948,6 +1952,7 @@ def test_edit_tag_only_color(client, settings): user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) task = f.TaskFactory.create(project=project, tags=["tag"]) issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) role = f.RoleFactory.create(project=project, permissions=["view_project"]) membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) @@ -1969,6 +1974,8 @@ def test_edit_tag_only_color(client, settings): assert task.tags == ["tag"] issue = Issue.objects.get(id=issue.pk) assert issue.tags == ["tag"] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == ["tag"] def test_edit_tag(client, settings): @@ -1977,6 +1984,7 @@ def test_edit_tag(client, settings): user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) task = f.TaskFactory.create(project=project, tags=["tag"]) issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) role = f.RoleFactory.create(project=project, permissions=["view_project"]) membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) @@ -1999,6 +2007,8 @@ def test_edit_tag(client, settings): assert task.tags == ["renamed_tag"] issue = Issue.objects.get(id=issue.pk) assert issue.tags == ["renamed_tag"] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == ["renamed_tag"] def test_delete_tag(client, settings): @@ -2007,6 +2017,7 @@ def test_delete_tag(client, settings): user_story = f.UserStoryFactory.create(project=project, tags=["tag"]) task = f.TaskFactory.create(project=project, tags=["tag"]) issue = f.IssueFactory.create(project=project, tags=["tag"]) + epic = f.EpicFactory.create(project=project, tags=["tag"]) role = f.RoleFactory.create(project=project, permissions=["view_project"]) membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) @@ -2027,6 +2038,8 @@ def test_delete_tag(client, settings): assert task.tags == [] issue = Issue.objects.get(id=issue.pk) assert issue.tags == [] + epic = Epic.objects.get(id=epic.pk) + assert epic.tags == [] def test_mix_tags(client, settings): @@ -2035,6 +2048,7 @@ def test_mix_tags(client, settings): user_story = f.UserStoryFactory.create(project=project, tags=["tag1", "tag3"]) task = f.TaskFactory.create(project=project, tags=["tag2", "tag3"]) issue = f.IssueFactory.create(project=project, tags=["tag1", "tag2", "tag3"]) + epic = f.EpicFactory.create(project=project, tags=["tag1", "tag2", "tag3"]) role = f.RoleFactory.create(project=project, permissions=["view_project"]) membership = f.MembershipFactory.create(project=project, user=user, role=role, is_admin=True) @@ -2056,6 +2070,8 @@ def test_mix_tags(client, settings): assert set(task.tags) == set(["tag2", "tag3"]) issue = Issue.objects.get(id=issue.pk) assert set(issue.tags) == set(["tag2", "tag3"]) + epic = Epic.objects.get(id=epic.pk) + assert set(epic.tags) == set(["tag2", "tag3"]) def test_color_tags_project_fired_on_element_create(): From ddb0d733c45cf14332cbee3133a1234e917ef2b9 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 21 Sep 2016 13:46:52 +0200 Subject: [PATCH 246/261] Rendering properly epic links --- taiga/mdrender/extensions/references.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/taiga/mdrender/extensions/references.py b/taiga/mdrender/extensions/references.py index d472d663..6828c739 100644 --- a/taiga/mdrender/extensions/references.py +++ b/taiga/mdrender/extensions/references.py @@ -57,7 +57,9 @@ class TaigaReferencesPattern(Pattern): subject = instance.content_object.subject - if instance.content_type.model == "userstory": + if instance.content_type.model == "epic": + html_classes = "reference epic" + elif instance.content_type.model == "userstory": html_classes = "reference user-story" elif instance.content_type.model == "task": html_classes = "reference task" From 1fb84e6b8da28c0f285d00aeae210e44eaae0835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 22 Sep 2016 09:22:44 +0200 Subject: [PATCH 247/261] Add related user stories in CSV report for epics --- taiga/projects/epics/services.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index 610bfcfb..ed8f127a 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -165,14 +165,15 @@ def epics_to_csv(project, queryset): fieldnames = ["ref", "subject", "description", "owner", "owner_full_name", "assigned_to", "assigned_to_full_name", "status", "epics_order", "client_requirement", "team_requirement", "attachments", "tags", "watchers", "voters", - "created_date", "modified_date"] + "created_date", "modified_date", "related_user_stories"] custom_attrs = project.epiccustomattributes.all() for custom_attr in custom_attrs: fieldnames.append(custom_attr.name) queryset = queryset.prefetch_related("attachments", - "custom_attributes_values") + "custom_attributes_values", + "user_stories__project") queryset = queryset.select_related("owner", "assigned_to", "status", @@ -202,7 +203,11 @@ def epics_to_csv(project, queryset): "voters": epic.total_voters, "created_date": epic.created_date, "modified_date": epic.modified_date, + "related_user_stories": ",".join([ + "{}#{}".format(us.project.slug, us.ref) for us in epic.user_stories.all() + ]), } + for custom_attr in custom_attrs: value = epic.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) epic_data[custom_attr.name] = value From fe28a9ddf97caea2038336b59b704634f4ba0ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 23 Sep 2016 10:04:53 +0200 Subject: [PATCH 248/261] Fix tests --- tests/integration/test_epics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py index 27314f02..a95f1c7c 100644 --- a/tests/integration/test_epics.py +++ b/tests/integration/test_epics.py @@ -64,9 +64,9 @@ def test_custom_fields_csv_generation(): data.seek(0) reader = csv.reader(data) row = next(reader) - assert row[17] == attr.name + assert row[18] == attr.name row = next(reader) - assert row[17] == "val1" + assert row[18] == "val1" def test_update_epic_order(client): From 7e7c567709785e03ee24bc0864db5c8b23622c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Mon, 26 Sep 2016 11:55:36 +0200 Subject: [PATCH 249/261] Reorder and add some explanations --- settings/local.py.example | 84 +++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/settings/local.py.example b/settings/local.py.example index 7e0c44a8..7defff37 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -18,6 +18,10 @@ from .development import * +######################################### +## GENERIC +######################################### + #DEBUG = False #ADMINS = ( @@ -54,6 +58,25 @@ DATABASES = { #STATIC_ROOT = '/home/taiga/static' +######################################### +## THROTTLING +######################################### + +#REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { +# "anon": "20/min", +# "user": "200/min", +# "import-mode": "20/sec", +# "import-dump-mode": "1/minute" +#} + + +######################################### +## MAIL SYSTEM SETTINGS +######################################### + +#DEFAULT_FROM_EMAIL = "john@doe.com" +#CHANGE_NOTIFICATIONS_MIN_INTERVAL = 300 #seconds + # EMAIL SETTINGS EXAMPLE #EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #EMAIL_USE_TLS = False @@ -61,7 +84,6 @@ DATABASES = { #EMAIL_PORT = 25 #EMAIL_HOST_USER = 'user' #EMAIL_HOST_PASSWORD = 'password' -#DEFAULT_FROM_EMAIL = "john@doe.com" # GMAIL SETTINGS EXAMPLE #EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' @@ -71,13 +93,22 @@ DATABASES = { #EMAIL_HOST_USER = 'youremail@gmail.com' #EMAIL_HOST_PASSWORD = 'yourpassword' -# THROTTLING -#REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { -# "anon": "20/min", -# "user": "200/min", -# "import-mode": "20/sec", -# "import-dump-mode": "1/minute" -#} + +######################################### +## REGISTRATION +######################################### + +#PUBLIC_REGISTER_ENABLED = True + +# LIMIT ALLOWED DOMAINS FOR REGISTER AND INVITE +# None or [] values in USER_EMAIL_ALLOWED_DOMAINS means allow any domain +#USER_EMAIL_ALLOWED_DOMAINS = None + +# PUCLIC OR PRIVATE NUMBER OF PROJECT PER USER +#MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit +#MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit +#MAX_MEMBERSHIPS_PRIVATE_PROJECTS = None # None == no limit +#MAX_MEMBERSHIPS_PUBLIC_PROJECTS = None # None == no limit # GITHUB SETTINGS #GITHUB_URL = "https://github.com/" @@ -85,27 +116,40 @@ DATABASES = { #GITHUB_API_CLIENT_ID = "yourgithubclientid" #GITHUB_API_CLIENT_SECRET = "yourgithubclientsecret" -# FEEDBACK MODULE (See config in taiga-front too) -#FEEDBACK_ENABLED = True -#FEEDBACK_EMAIL = "support@taiga.io" -# STATS MODULE -#STATS_ENABLED = False -#FRONT_SITEMAP_CACHE_TIMEOUT = 60*60 # In second +######################################### +## SITEMAP +######################################### -# SITEMAP # If is True /front/sitemap.xml show a valid sitemap of taiga-front client #FRONT_SITEMAP_ENABLED = False #FRONT_SITEMAP_CACHE_TIMEOUT = 24*60*60 # In second -# CELERY + +######################################### +## FEEDBACK +######################################### + +# Note: See config in taiga-front too +#FEEDBACK_ENABLED = True +#FEEDBACK_EMAIL = "support@taiga.io" + + +######################################### +## STATS +######################################### + +#STATS_ENABLED = False +#FRONT_SITEMAP_CACHE_TIMEOUT = 60*60 # In second + + +######################################### +## CELERY +######################################### + #from .celery import * #CELERY_ENABLED = True # # To use celery in memory #CELERY_ENABLED = True #CELERY_ALWAYS_EAGER = True - -# LIMIT ALLOWED DOMAINS FOR REGISTER AND INVITE -# None or [] values in USER_EMAIL_ALLOWED_DOMAINS means allow any domain -# USER_EMAIL_ALLOWED_DOMAINS = None From 19cd0c53540e7c2abc0ecb550b26c282f317bddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 22 Sep 2016 13:55:48 +0200 Subject: [PATCH 250/261] Fix epic sort when 'order' is the same --- taiga/projects/userstories/services.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 71377b52..0d781429 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -474,18 +474,7 @@ def _get_userstories_epics(project, queryset): WHERE {where} GROUP BY "epics_relateduserstory"."epic_id" ) - SELECT "epics_epic"."id" AS "id", - "epics_epic"."ref" AS "ref", - "epics_epic"."subject" AS "subject", - "epics_epic"."epics_order" AS "order", - COALESCE("counters"."counter", 0) AS "counter" - FROM "epics_epic" - LEFT OUTER JOIN "counters" - ON ("counters"."epic_id" = "epics_epic"."id") - WHERE "epics_epic"."project_id" = %s - -- User stories with no epics (return results only if there are userstories) - UNION SELECT NULL AS "id", NULL AS "ref", NULL AS "subject", @@ -498,6 +487,16 @@ def _get_userstories_epics(project, queryset): ON ("userstories_userstory"."project_id" = "projects_project"."id") WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL GROUP BY "epics_relateduserstory"."epic_id" + UNION + SELECT "epics_epic"."id" AS "id", + "epics_epic"."ref" AS "ref", + "epics_epic"."subject" AS "subject", + "epics_epic"."epics_order" AS "order", + COALESCE("counters"."counter", 0) AS "counter" + FROM "epics_epic" + LEFT OUTER JOIN "counters" + ON ("counters"."epic_id" = "epics_epic"."id") + WHERE "epics_epic"."project_id" = %s """.format(where=where) with closing(connection.cursor()) as cursor: @@ -514,7 +513,7 @@ def _get_userstories_epics(project, queryset): "count": count, }) - result = sorted(result, key=itemgetter("order")) + result = sorted(result, key=lambda k: (k["order"], k["id"] or 0)) # Add row when there is no user stories with no epics if result == [] or result[0]["id"] is not None: From ae0bc7ee884d838d1d655c98460312b4f398d010 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 22 Sep 2016 12:28:24 +0200 Subject: [PATCH 251/261] Filter by epics --- taiga/projects/userstories/api.py | 11 +- taiga/projects/userstories/filters.py | 4 - taiga/projects/userstories/services.py | 234 +++++++++++++++---------- tests/integration/test_userstories.py | 30 +++- 4 files changed, 178 insertions(+), 101 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index df1b495a..6ee1d72c 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -59,7 +59,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) filter_backends = (base_filters.CanViewUsFilterBackend, - filters.EpicsFilter, filters.EpicFilter, base_filters.OwnersFilter, base_filters.AssignedToFilter, @@ -104,7 +103,13 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi include_attachments = "include_attachments" in self.request.QUERY_PARAMS include_tasks = "include_tasks" in self.request.QUERY_PARAMS - epic_id = self.request.QUERY_PARAMS.get("epic", None) + epic_id = self.request.QUERY_PARAMS.get("epic", None) + # We can be filtering by more than one epic so epic_id can consist + # of different ids separete by comma. In that situation we will use + # only the first + if epic_id is not None: + epic_id = epic_id.split(",")[0] + qs = attach_extra_info(qs, user=self.request.user, include_attachments=include_attachments, include_tasks=include_tasks, @@ -278,7 +283,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi statuses_filter_backends = (f for f in filter_backends if f != base_filters.StatusesFilter) assigned_to_filter_backends = (f for f in filter_backends if f != base_filters.AssignedToFilter) owners_filter_backends = (f for f in filter_backends if f != base_filters.OwnersFilter) - epics_filter_backends = (f for f in filter_backends if f != filters.EpicsFilter) + epics_filter_backends = (f for f in filter_backends if f != filters.EpicFilter) queryset = self.get_queryset() querysets = { diff --git a/taiga/projects/userstories/filters.py b/taiga/projects/userstories/filters.py index f061667e..ec19b6a7 100644 --- a/taiga/projects/userstories/filters.py +++ b/taiga/projects/userstories/filters.py @@ -23,7 +23,3 @@ from taiga.base import filters class EpicFilter(filters.BaseRelatedFieldsFilter): filter_name = "epics" param_name = "epic" - - -class EpicsFilter(filters.BaseRelatedFieldsFilter): - filter_name = "epics" diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 0d781429..7cb0be4f 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -273,18 +273,31 @@ def _get_userstories_statuses(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - SELECT "projects_userstorystatus"."id", - "projects_userstorystatus"."name", - "projects_userstorystatus"."color", - "projects_userstorystatus"."order", - (SELECT count(*) - FROM "userstories_userstory" - INNER JOIN "projects_project" ON - ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} AND "userstories_userstory"."status_id" = "projects_userstorystatus"."id") - FROM "projects_userstorystatus" - WHERE "projects_userstorystatus"."project_id" = %s - ORDER BY "projects_userstorystatus"."order"; + WITH "us_counters" AS ( + SELECT DISTINCT "userstories_userstory"."status_id" "status_id", + "userstories_userstory"."id" "us_id" + FROM "userstories_userstory" + LEFT JOIN "epics_relateduserstory" + ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" + WHERE {where} + ), + "counters" AS ( + SELECT "status_id", + COUNT("status_id") "count" + FROM "us_counters" + GROUP BY "status_id" + ) + + SELECT "projects_userstorystatus"."id", + "projects_userstorystatus"."name", + "projects_userstorystatus"."color", + "projects_userstorystatus"."order", + COALESCE("counters"."count", 0) + FROM "projects_userstorystatus" + LEFT JOIN "counters" + ON "counters"."status_id" = "projects_userstorystatus"."id" + WHERE "projects_userstorystatus"."project_id" = %s + ORDER BY "projects_userstorystatus"."order"; """.format(where=where) with closing(connection.cursor()) as cursor: @@ -310,31 +323,47 @@ def _get_userstories_assigned_to(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH counters AS ( - SELECT assigned_to_id, count(assigned_to_id) count - FROM "userstories_userstory" - INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NOT NULL - GROUP BY assigned_to_id - ) + WITH "us_counters" AS ( + SELECT DISTINCT "userstories_userstory"."assigned_to_id" "assigned_to_id", + "userstories_userstory"."id" "us_id" + FROM "userstories_userstory" + LEFT JOIN "epics_relateduserstory" + ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" + WHERE {where} + ), - SELECT "projects_membership"."user_id" user_id, - "users_user"."full_name", - "users_user"."username", - COALESCE("counters".count, 0) count - FROM projects_membership - LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id") - INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + "counters" AS ( + SELECT "assigned_to_id", + COUNT("assigned_to_id") + FROM "us_counters" + GROUP BY "assigned_to_id" + ) + + SELECT "projects_membership"."user_id" "user_id", + "users_user"."full_name" "full_name", + "users_user"."username" "username", + COALESCE("counters".count, 0) "count" + FROM "projects_membership" + LEFT OUTER JOIN "counters" + ON ("projects_membership"."user_id" = "counters"."assigned_to_id") + INNER JOIN "users_user" + ON ("projects_membership"."user_id" = "users_user"."id") WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL -- unassigned userstories UNION - SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count + SELECT NULL "user_id", + NULL "full_name", + NULL "username", + count(coalesce("assigned_to_id", -1)) "count" FROM "userstories_userstory" - INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL - GROUP BY assigned_to_id + GROUP BY "assigned_to_id" """.format(where=where) with closing(connection.cursor()) as cursor: @@ -371,33 +400,43 @@ def _get_userstories_owners(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH counters AS ( - SELECT "userstories_userstory"."owner_id" owner_id, - count(coalesce("userstories_userstory"."owner_id", -1)) count - FROM "userstories_userstory" - INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} - GROUP BY "userstories_userstory"."owner_id" - ) + WITH "us_counters" AS( + SELECT DISTINCT "userstories_userstory"."owner_id" "owner_id", + "userstories_userstory"."id" "us_id" + FROM "userstories_userstory" + LEFT OUTER JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + WHERE {where} + ), - SELECT "projects_membership"."user_id" id, + "counters" AS ( + SELECT "owner_id", + COUNT("owner_id") + FROM "us_counters" + GROUP BY "owner_id" + ) + + SELECT "projects_membership"."user_id" "user_id", "users_user"."full_name", "users_user"."username", - COALESCE("counters".count, 0) count - FROM projects_membership - LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") - INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + COALESCE("counters".count, 0) "count" + FROM "projects_membership" + LEFT OUTER JOIN "counters" + ON ("projects_membership"."user_id" = "counters"."owner_id") + INNER JOIN "users_user" + ON ("projects_membership"."user_id" = "users_user"."id") WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL -- System users - UNION + UNION - SELECT "users_user"."id" user_id, - "users_user"."full_name" full_name, - "users_user"."username" username, - COALESCE("counters".count, 0) count - FROM users_user - LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id") + SELECT "users_user"."id" "user_id", + "users_user"."full_name" "full_name", + "users_user"."username" "username", + COALESCE("counters"."count", 0) "count" + FROM "users_user" + LEFT OUTER JOIN "counters" + ON ("users_user"."id" = "counters"."owner_id") WHERE ("users_user"."is_system" IS TRUE) """.format(where=where) @@ -423,24 +462,31 @@ def _get_userstories_tags(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH userstories_tags AS ( - SELECT tag, - COUNT(tag) counter FROM ( - SELECT UNNEST(userstories_userstory.tags) tag - FROM userstories_userstory - INNER JOIN projects_project - ON (userstories_userstory.project_id = projects_project.id) - WHERE {where}) tags - GROUP BY tag), - project_tags AS ( - SELECT reduce_dim(tags_colors) tag_color - FROM projects_project - WHERE id=%s) + WITH "userstories_tags" AS ( + SELECT "tag", + COUNT("tag") "counter" + FROM ( + SELECT DISTINCT "userstories_userstory"."id" "us_id", + UNNEST("userstories_userstory"."tags") "tag" + FROM "userstories_userstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + WHERE {where} + ) "tags" + GROUP BY "tag"), - SELECT tag_color[1] tag, COALESCE(userstories_tags.counter, 0) counter - FROM project_tags - LEFT JOIN userstories_tags ON project_tags.tag_color[1] = userstories_tags.tag - ORDER BY tag + "project_tags" AS ( + SELECT reduce_dim("tags_colors") "tag_color" + FROM "projects_project" + WHERE "id"=%s) + + SELECT "tag_color"[1] "tag", COALESCE("userstories_tags"."counter", 0) "counter" + FROM "project_tags" + LEFT JOIN "userstories_tags" + ON "project_tags"."tag_color"[1] = "userstories_tags"."tag" + ORDER BY "tag" """.format(where=where) with closing(connection.cursor()) as cursor: @@ -461,33 +507,35 @@ def _get_userstories_epics(project, queryset): queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) where = queryset_where_tuple[0] where_params = queryset_where_tuple[1] - extra_sql = """ - WITH counters AS ( - SELECT "epics_relateduserstory"."epic_id" AS "epic_id", - count("epics_relateduserstory"."id") AS "counter" - FROM "epics_relateduserstory" - INNER JOIN "userstories_userstory" - ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") - INNER JOIN "projects_project" - ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} - GROUP BY "epics_relateduserstory"."epic_id" - ) + WITH "counters" AS ( + SELECT "epics_relateduserstory"."epic_id" AS "epic_id", + count("epics_relateduserstory"."id") AS "counter" + FROM "epics_relateduserstory" + INNER JOIN "userstories_userstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} + GROUP BY "epics_relateduserstory"."epic_id" + ) + -- User stories with no epics (return results only if there are userstories) - SELECT NULL AS "id", - NULL AS "ref", - NULL AS "subject", - 0 AS "order", - count(COALESCE("epics_relateduserstory"."epic_id", -1)) AS "counter" - FROM "userstories_userstory" - LEFT OUTER JOIN "epics_relateduserstory" - ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id") - INNER JOIN "projects_project" - ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL - GROUP BY "epics_relateduserstory"."epic_id" - UNION + SELECT NULL AS "id", + NULL AS "ref", + NULL AS "subject", + 0 AS "order", + count(COALESCE("epics_relateduserstory"."epic_id", -1)) AS "counter" + FROM "userstories_userstory" + LEFT OUTER JOIN "epics_relateduserstory" + ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL + GROUP BY "epics_relateduserstory"."epic_id" + + UNION + SELECT "epics_epic"."id" AS "id", "epics_epic"."ref" AS "ref", "epics_epic"."subject" AS "subject", @@ -498,9 +546,9 @@ def _get_userstories_epics(project, queryset): ON ("counters"."epic_id" = "epics_epic"."id") WHERE "epics_epic"."project_id" = %s """.format(where=where) - + with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, where_params + [project.id] + where_params) + cursor.execute(extra_sql, where_params + where_params + [project.id]) rows = cursor.fetchall() result = [] diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 57a2f520..5d7bec2a 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -684,7 +684,7 @@ def test_api_filters_data(client): # | 6 | status3 | user2 | user1 | tag1 tag2 | epic0 epic2 | # | 7 | status0 | user1 | user2 | tag3 | None | # | 8 | status3 | user3 | user2 | tag1 | epic2 | - # | 9 | status1 | user2 | user3 | tag0 | none | + # | 9 | status1 | user2 | user3 | tag0 | None | # ------------------------------------------------------------------------------ us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, @@ -802,6 +802,34 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 0 assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 1 + # Filter (epic0 epic2) + response = client.get(url + "&epic={},{}".format(epic0.id, epic2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 3 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + def test_get_invalid_csv(client): url = reverse("userstories-csv") From aa0095ba582a5a2e8e9b11c9ea7c6c0b3f5a9a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 27 Sep 2016 09:42:28 +0200 Subject: [PATCH 252/261] Add missing migration --- .../migrations/0053_auto_20160927_0741.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 taiga/projects/migrations/0053_auto_20160927_0741.py diff --git a/taiga/projects/migrations/0053_auto_20160927_0741.py b/taiga/projects/migrations/0053_auto_20160927_0741.py new file mode 100644 index 00000000..0b4d3136 --- /dev/null +++ b/taiga/projects/migrations/0053_auto_20160927_0741.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-27 07:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0052_epic_status'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='creation_template', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='projects.ProjectTemplate', verbose_name='creation template'), + ), + ] From dc623f5c5ca0804b41b5cb77c8354c6aaf07af1d Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 20 Sep 2016 11:21:43 +0200 Subject: [PATCH 253/261] Improving history migration 0011 --- .../projects/epics/migrations/0001_initial.py | 1 + .../migrations/0011_auto_20160629_1036.py | 156 ++++++++++++++++-- 2 files changed, 144 insertions(+), 13 deletions(-) diff --git a/taiga/projects/epics/migrations/0001_initial.py b/taiga/projects/epics/migrations/0001_initial.py index 78d2dc8f..e757b7be 100644 --- a/taiga/projects/epics/migrations/0001_initial.py +++ b/taiga/projects/epics/migrations/0001_initial.py @@ -17,6 +17,7 @@ class Migration(migrations.Migration): dependencies = [ ('userstories', '0012_auto_20160614_1201'), ('projects', '0049_auto_20160629_1443'), + ('history', '0012_auto_20160629_1036'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] diff --git a/taiga/projects/history/migrations/0011_auto_20160629_1036.py b/taiga/projects/history/migrations/0011_auto_20160629_1036.py index d3f1eb02..698f6a21 100644 --- a/taiga/projects/history/migrations/0011_auto_20160629_1036.py +++ b/taiga/projects/history/migrations/0011_auto_20160629_1036.py @@ -2,30 +2,160 @@ # Generated by Django 1.9.2 on 2016-06-29 10:36 from __future__ import unicode_literals -from django.db import migrations +from django.db import migrations, connection from taiga.projects.history.services import get_instance_from_key -def forward_func(apps, schema_editor): - HistoryEntry = apps.get_model("history", "HistoryEntry") - db_alias = schema_editor.connection.alias - for entry in HistoryEntry.objects.using(db_alias).all().iterator(): - instance = get_instance_from_key(entry.key) - if type(instance) == apps.get_model("projects", "Project"): - entry.project_id = instance.id - else: - entry.project_id = getattr(instance, 'project_id', None) - entry.save() +GENERATE_CORRECT_HISTORY_ENTRIES_TABLE = """ + -- Creating a table containing all the existing object keys and the project ids + DROP TABLE IF EXISTS project_keys; + CREATE TABLE project_keys ( + key VARCHAR, + project_id INTEGER + ); - HistoryEntry.objects.using(db_alias).filter(project_id__isnull=True).delete() + DROP INDEX IF EXISTS project_keys_index; + CREATE INDEX project_keys_index + ON project_keys + USING btree + (key); + + INSERT INTO project_keys + SELECT 'milestones.milestone:' || id, project_id + FROM milestones_milestone; + + INSERT INTO project_keys + SELECT 'userstories.userstory:' || id, project_id + FROM userstories_userstory; + + INSERT INTO project_keys + SELECT 'tasks.task:' || id, project_id + FROM tasks_task; + + INSERT INTO project_keys + SELECT 'issues.issue:' || id, project_id + FROM issues_issue; + + INSERT INTO project_keys + SELECT 'wiki.wikipage:' || id, project_id + FROM wiki_wikipage; + + INSERT INTO project_keys + SELECT 'projects.project:' || id, id + FROM projects_project; + + -- Create a table where we will insert all the history_historyentry content with its correct project_id + -- Elements without project_id won't be inserted + DROP TABLE IF EXISTS history_historyentry_correct; + CREATE TABLE history_historyentry_correct AS + SELECT + history_historyentry.id , + history_historyentry.user, + history_historyentry.created_at, + history_historyentry.type, + history_historyentry.is_snapshot, + history_historyentry.key, + history_historyentry.diff, + history_historyentry.snapshot, + history_historyentry.values, + history_historyentry.comment, + history_historyentry.comment_html, + history_historyentry.delete_comment_date, + history_historyentry.delete_comment_user, + history_historyentry.is_hidden, + history_historyentry.comment_versions, + history_historyentry.edit_comment_date, + project_keys.project_id + FROM history_historyentry + INNER JOIN project_keys + ON project_keys.key = history_historyentry.key; + + -- Delete aux table + DROP TABLE IF EXISTS project_keys; + """ + +def get_constraints_def_sql(table_name): + cursor = connection.cursor() + query = """ + SELECT 'ALTER TABLE "'||nspname||'"."'||relname||'" ADD CONSTRAINT "'||conname||'" '|| + pg_get_constraintdef(pg_constraint.oid)||';' + FROM pg_constraint + INNER JOIN pg_class ON conrelid=pg_class.oid + INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace + WHERE relname='{}' + ORDER BY CASE WHEN contype='f' THEN 0 ELSE 1 END DESC,contype DESC,nspname DESC,relname DESC,conname DESC; + """.format(table_name) + cursor.execute(query) + return [row[0] for row in cursor.fetchall()] + + +def get_indexes_def_sql(table_name): + cursor = connection.cursor() + query = """ + SELECT pg_get_indexdef(idx.oid)||';' + FROM pg_index ind + JOIN pg_class idx ON idx.oid = ind.indexrelid + JOIN pg_class tbl ON tbl.oid = ind.indrelid + LEFT JOIN pg_namespace ns ON ns.oid = tbl.relnamespace + WHERE + tbl.relname = '{}' AND + indisprimary=FALSE; + """.format(table_name) + cursor.execute(query) + return [row[0] for row in cursor.fetchall()] + + +def drop_constraints(table_name): + # This query returns all the ALTER sentences needed to drop the constraints + cursor = connection.cursor() + alter_sentences_query = """ + SELECT 'ALTER TABLE "'||nspname||'"."'||relname||'" DROP CONSTRAINT "'||conname||'" '||';' + FROM pg_constraint + INNER JOIN pg_class ON conrelid=pg_class.oid + INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace + WHERE relname='{}' + ORDER BY CASE WHEN contype='f' THEN 0 ELSE 1 END DESC,contype DESC,nspname DESC,relname DESC,conname DESC; + """.format(table_name) + cursor.execute(alter_sentences_query) + alter_sentences = [row[0] for row in cursor.fetchall()] + + #Now we execute those sentences + for alter_sentence in alter_sentences: + cursor.execute(alter_sentence) + + +def toggle_history_entries_tables(apps, schema_editor): + history_entry_sql_def_contraints = get_constraints_def_sql("history_historyentry") + history_entry_sql_def_indexes = get_indexes_def_sql("history_historyentry") + history_change_notifications_sql_def_contraints = get_constraints_def_sql("notifications_historychangenotification_history_entries") + drop_constraints("notifications_historychangenotification_history_entries") + cursor = connection.cursor() + cursor.execute(""" + DELETE FROM notifications_historychangenotification_history_entries; + DROP TABLE history_historyentry; + ALTER TABLE "history_historyentry_correct" RENAME to "history_historyentry"; + """) + + for history_entry_sql_def_contraint in history_entry_sql_def_contraints: + cursor.execute(history_entry_sql_def_contraint) + + for history_entry_sql_def_index in history_entry_sql_def_indexes: + cursor.execute(history_entry_sql_def_index) + + # Restoring the dropped constraints and indexes + for history_change_notifications_sql_def_contraint in history_change_notifications_sql_def_contraints: + cursor.execute(history_change_notifications_sql_def_contraint) class Migration(migrations.Migration): dependencies = [ ('history', '0010_historyentry_project'), + ('wiki', '0003_auto_20160615_0721'), + ('users', '0022_auto_20160629_1443') ] operations = [ - migrations.RunPython(forward_func, atomic=False), + migrations.RunSQL(GENERATE_CORRECT_HISTORY_ENTRIES_TABLE), + migrations.RunPython(toggle_history_entries_tables) ] From e8dae79ce0eaf3ace1e6bbe488f21d9b81e4af54 Mon Sep 17 00:00:00 2001 From: Stefan Auditor Date: Wed, 21 Sep 2016 09:23:37 +0200 Subject: [PATCH 254/261] Rework webhook signature header to align with larger implementations and defined standards at https://superfeedr-misc.s3.amazonaws.com/pubsubhubbub-core-0.4.html\#authednotify --- AUTHORS.rst | 1 + CHANGELOG.md | 1 + taiga/webhooks/tasks.py | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 0f61b4da..18559763 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -33,5 +33,6 @@ answer newbie questions, and generally made taiga that much better: - Motius GmbH - Riccardo Coccioli - Ricky Posner +- Stefan Auditor - Yamila Moreno - Yaser Alraddadi diff --git a/CHANGELOG.md b/CHANGELOG.md index 728ad58c..9174775d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Improve messages generated on webhooks input. - Add mentions support in commit messages. - Cleanup hooks code. + - Rework webhook signature header to align with larger implementations and defined [standards](https://superfeedr-misc.s3.amazonaws.com/pubsubhubbub-core-0.4.html\#authednotify). (thanks to [Stefan Auditor](https://github.com/sanduhrs)) - Add created-, modified-, finished- and finish_date queryset filters - Support exact match, gt, gte, lt, lte - added issues, tasks and userstories accordingly diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index 75e3caad..9e9489ab 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -66,7 +66,8 @@ def _send_request(webhook_id, url, key, data): serialized_data = UnicodeJSONRenderer().render(data) signature = _generate_signature(serialized_data, key) headers = { - "X-TAIGA-WEBHOOK-SIGNATURE": signature, + "X-TAIGA-WEBHOOK-SIGNATURE": signature, # For backward compatibility + "X-Hub-Signature": "sha1={}".format(signature), "Content-Type": "application/json" } request = requests.Request('POST', url, data=serialized_data, headers=headers) From ec9cff8a8832e1f82d3be83d71745d37be3ba2dc Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 28 Sep 2016 07:44:18 +0200 Subject: [PATCH 255/261] Improving default values for orders --- taiga/base/utils/time.py | 23 ++++++++++++ .../migrations/0010_auto_20160928_0540.py | 36 +++++++++++++++++++ taiga/projects/custom_attributes/models.py | 3 +- .../migrations/0004_auto_20160928_0540.py | 26 ++++++++++++++ taiga/projects/epics/models.py | 5 +-- .../migrations/0054_auto_20160928_0540.py | 26 ++++++++++++++ taiga/projects/models.py | 5 +-- .../migrations/0011_auto_20160928_0755.py | 26 ++++++++++++++ taiga/projects/tasks/models.py | 5 +-- .../migrations/0014_auto_20160928_0540.py | 31 ++++++++++++++++ taiga/projects/userstories/models.py | 7 ++-- .../migrations/0004_auto_20160928_0540.py | 21 +++++++++++ taiga/projects/wiki/models.py | 3 +- 13 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 taiga/base/utils/time.py create mode 100644 taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py create mode 100644 taiga/projects/epics/migrations/0004_auto_20160928_0540.py create mode 100644 taiga/projects/migrations/0054_auto_20160928_0540.py create mode 100644 taiga/projects/tasks/migrations/0011_auto_20160928_0755.py create mode 100644 taiga/projects/userstories/migrations/0014_auto_20160928_0540.py create mode 100644 taiga/projects/wiki/migrations/0004_auto_20160928_0540.py diff --git a/taiga/base/utils/time.py b/taiga/base/utils/time.py new file mode 100644 index 00000000..cd7b00c4 --- /dev/null +++ b/taiga/base/utils/time.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import time + + +def timestamp_ms(): + return int(time.time() * 1000) diff --git a/taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py b/taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py new file mode 100644 index 00000000..afe2277a --- /dev/null +++ b/taiga/projects/custom_attributes/migrations/0010_auto_20160928_0540.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-28 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.time + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_attributes', '0009_auto_20160728_1002'), + ] + + operations = [ + migrations.AlterField( + model_name='epiccustomattribute', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'), + ), + migrations.AlterField( + model_name='issuecustomattribute', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'), + ), + migrations.AlterField( + model_name='taskcustomattribute', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'), + ), + migrations.AlterField( + model_name='userstorycustomattribute', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'), + ), + ] diff --git a/taiga/projects/custom_attributes/models.py b/taiga/projects/custom_attributes/models.py index 4fa6978b..6467f97e 100644 --- a/taiga/projects/custom_attributes/models.py +++ b/taiga/projects/custom_attributes/models.py @@ -22,6 +22,7 @@ from django.utils import timezone from django_pgjson.fields import JsonField +from taiga.base.utils.time import timestamp_ms from taiga.projects.occ.mixins import OCCModelMixin from . import choices @@ -37,7 +38,7 @@ class AbstractCustomAttribute(models.Model): type = models.CharField(null=False, blank=False, max_length=16, choices=choices.TYPES_CHOICES, default=choices.TEXT_TYPE, verbose_name=_("type")) - order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("order")) + order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order")) project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss", verbose_name=_("project")) diff --git a/taiga/projects/epics/migrations/0004_auto_20160928_0540.py b/taiga/projects/epics/migrations/0004_auto_20160928_0540.py new file mode 100644 index 00000000..0e6a9fcb --- /dev/null +++ b/taiga/projects/epics/migrations/0004_auto_20160928_0540.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-28 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.time + + +class Migration(migrations.Migration): + + dependencies = [ + ('epics', '0003_auto_20160901_1021'), + ] + + operations = [ + migrations.AlterField( + model_name='epic', + name='epics_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='epics order'), + ), + migrations.AlterField( + model_name='relateduserstory', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'), + ), + ] diff --git a/taiga/projects/epics/models.py b/taiga/projects/epics/models.py index c2e26d20..da0e4a3e 100644 --- a/taiga/projects/epics/models.py +++ b/taiga/projects/epics/models.py @@ -23,6 +23,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from taiga.base.utils.colors import generate_random_predefined_hex_color +from taiga.base.utils.time import timestamp_ms from taiga.projects.tagging.models import TaggedMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin @@ -40,7 +41,7 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M status = models.ForeignKey("projects.EpicStatus", null=True, blank=True, related_name="epics", verbose_name=_("status"), on_delete=models.SET_NULL) - epics_order = models.IntegerField(null=False, blank=False, default=10000, + epics_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("epics order")) created_date = models.DateTimeField(null=False, blank=False, @@ -96,7 +97,7 @@ class RelatedUserStory(WatchedModelMixin, models.Model): user_story = models.ForeignKey("userstories.UserStory", on_delete=models.CASCADE) epic = models.ForeignKey("epics.Epic", on_delete=models.CASCADE) - order = models.IntegerField(null=False, blank=False, default=10000, + order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order")) class Meta: diff --git a/taiga/projects/migrations/0054_auto_20160928_0540.py b/taiga/projects/migrations/0054_auto_20160928_0540.py new file mode 100644 index 00000000..6fe8def5 --- /dev/null +++ b/taiga/projects/migrations/0054_auto_20160928_0540.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-28 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.time + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0053_auto_20160927_0741'), + ] + + operations = [ + migrations.AlterField( + model_name='membership', + name='user_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='user order'), + ), + migrations.AlterField( + model_name='projecttemplate', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='user order'), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index fe62c015..f6f6cc52 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -31,6 +31,7 @@ from django_pglocks import advisory_lock from django_pgjson.fields import JsonField +from taiga.base.utils.time import timestamp_ms from taiga.projects.tagging.models import TaggedMixin from taiga.projects.tagging.models import TagsColorsdMixin from taiga.base.utils.files import get_file_path @@ -84,7 +85,7 @@ class Membership(models.Model): invitation_extra_text = models.TextField(null=True, blank=True, verbose_name=_("invitation extra text")) - user_order = models.IntegerField(default=10000, null=False, blank=False, + user_order = models.BigIntegerField(default=timestamp_ms, null=False, blank=False, verbose_name=_("user order")) class Meta: @@ -730,7 +731,7 @@ class ProjectTemplate(models.Model): verbose_name=_("slug"), unique=True) description = models.TextField(null=False, blank=False, verbose_name=_("description")) - order = models.IntegerField(default=10000, null=False, blank=False, + order = models.BigIntegerField(default=timestamp_ms, null=False, blank=False, verbose_name=_("user order")) created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), diff --git a/taiga/projects/tasks/migrations/0011_auto_20160928_0755.py b/taiga/projects/tasks/migrations/0011_auto_20160928_0755.py new file mode 100644 index 00000000..1802a9c3 --- /dev/null +++ b/taiga/projects/tasks/migrations/0011_auto_20160928_0755.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-28 07:55 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.time + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0010_auto_20160614_1201'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='taskboard_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='taskboard order'), + ), + migrations.AlterField( + model_name='task', + name='us_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='us order'), + ), + ] diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 15f768d4..a0abe570 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -23,6 +23,7 @@ from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from taiga.base.utils.time import timestamp_ms from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.mixins.blocked import BlockedMixin @@ -53,9 +54,9 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M subject = models.TextField(null=False, blank=False, verbose_name=_("subject")) - us_order = models.IntegerField(null=False, blank=False, default=1, + us_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("us order")) - taskboard_order = models.IntegerField(null=False, blank=False, default=1, + taskboard_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("taskboard order")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) diff --git a/taiga/projects/userstories/migrations/0014_auto_20160928_0540.py b/taiga/projects/userstories/migrations/0014_auto_20160928_0540.py new file mode 100644 index 00000000..38285839 --- /dev/null +++ b/taiga/projects/userstories/migrations/0014_auto_20160928_0540.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-28 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.time + + +class Migration(migrations.Migration): + + dependencies = [ + ('userstories', '0013_auto_20160722_1018'), + ] + + operations = [ + migrations.AlterField( + model_name='userstory', + name='backlog_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='backlog order'), + ), + migrations.AlterField( + model_name='userstory', + name='kanban_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='kanban order'), + ), + migrations.AlterField( + model_name='userstory', + name='sprint_order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='sprint order'), + ), + ] diff --git a/taiga/projects/userstories/models.py b/taiga/projects/userstories/models.py index d0216d21..178f2cc1 100644 --- a/taiga/projects/userstories/models.py +++ b/taiga/projects/userstories/models.py @@ -25,6 +25,7 @@ from django.utils import timezone from picklefield.fields import PickledObjectField +from taiga.base.utils.time import timestamp_ms from taiga.projects.tagging.models import TaggedMixin from taiga.projects.occ import OCCModelMixin from taiga.projects.notifications.mixins import WatchedModelMixin @@ -75,11 +76,11 @@ class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, mod related_name="userstories", through="RolePoints", verbose_name=_("points")) - backlog_order = models.IntegerField(null=False, blank=False, default=10000, + backlog_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("backlog order")) - sprint_order = models.IntegerField(null=False, blank=False, default=10000, + sprint_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("sprint order")) - kanban_order = models.IntegerField(null=False, blank=False, default=10000, + kanban_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("kanban order")) created_date = models.DateTimeField(null=False, blank=False, diff --git a/taiga/projects/wiki/migrations/0004_auto_20160928_0540.py b/taiga/projects/wiki/migrations/0004_auto_20160928_0540.py new file mode 100644 index 00000000..cc3fbacb --- /dev/null +++ b/taiga/projects/wiki/migrations/0004_auto_20160928_0540.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-09-28 05:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import taiga.base.utils.time + + +class Migration(migrations.Migration): + + dependencies = [ + ('wiki', '0003_auto_20160615_0721'), + ] + + operations = [ + migrations.AlterField( + model_name='wikilink', + name='order', + field=models.BigIntegerField(default=taiga.base.utils.time.timestamp_ms, verbose_name='order'), + ), + ] diff --git a/taiga/projects/wiki/models.py b/taiga/projects/wiki/models.py index 5a4b3485..1c51fff0 100644 --- a/taiga/projects/wiki/models.py +++ b/taiga/projects/wiki/models.py @@ -24,6 +24,7 @@ from django.utils import timezone from django_pglocks import advisory_lock from taiga.base.utils.slug import slugify_uniquely_for_queryset +from taiga.base.utils.time import timestamp_ms from taiga.projects.notifications.mixins import WatchedModelMixin from taiga.projects.occ import OCCModelMixin @@ -72,7 +73,7 @@ class WikiLink(models.Model): title = models.CharField(max_length=500, null=False, blank=False) href = models.SlugField(max_length=500, db_index=True, null=False, blank=False, verbose_name=_("href")) - order = models.PositiveSmallIntegerField(null=False, blank=False, default="10000", + order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order")) class Meta: From 1bf87ec469f0b71a2a168d34a0142c5fa470201e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Sep 2016 10:28:35 +0200 Subject: [PATCH 256/261] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9174775d..7d7aa93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog # -## 3.0.0 ??? (unreleased) + +## 3.0.0 Stellaria Borealis (2016-10-02) ### Features - Add Epics. From 71a1069ec23282bf8099768a540b28600d79f758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Sep 2016 10:30:51 +0200 Subject: [PATCH 257/261] [i18n] Update locales --- taiga/locale/ca/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/de/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/en/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/es/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/fi/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/fr/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/it/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/nb/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/nl/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/pl/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/pt_BR/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/ru/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/sv/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/tr/LC_MESSAGES/django.po | 324 ++++++++++----------- taiga/locale/zh-Hant/LC_MESSAGES/django.po | 324 ++++++++++----------- 15 files changed, 2430 insertions(+), 2430 deletions(-) diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po index 2759a15e..e8e00410 100644 --- a/taiga/locale/ca/LC_MESSAGES/django.po +++ b/taiga/locale/ca/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -192,7 +192,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -754,13 +754,13 @@ msgid "Authentication required" msgstr "" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "Nom" @@ -774,12 +774,12 @@ msgid "web" msgstr "" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "Descripció" @@ -813,13 +813,13 @@ msgid "comment" msgstr "Comentari" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "Data de creació" @@ -890,7 +890,7 @@ msgstr "El payload no és un arxiu json vàlid" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "El projecte no existeix" @@ -1163,10 +1163,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "Amo" @@ -1245,17 +1245,17 @@ msgid "Project ID not matches between object and project" msgstr "" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "Projecte" @@ -1268,11 +1268,11 @@ msgid "object id" msgstr "Id d'objecte" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "Data de modificació" @@ -1290,13 +1290,13 @@ msgid "is deprecated" msgstr "està obsolet " #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "Ordre" @@ -1348,29 +1348,29 @@ msgstr "" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "tipus" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "història d'usuari" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "tasca" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "incidéncia" @@ -1382,47 +1382,47 @@ msgstr "Ja existix altre amb el matex nom." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "estatus" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "tema" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "color" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "assignada a" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "requeriment de client" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "requeriment d'equip" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1558,7 +1558,7 @@ msgid "To:" msgstr "A:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "contingut" @@ -1599,17 +1599,17 @@ msgstr "severitat" msgid "priority" msgstr "prioritat" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "fita" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "Data de finalització" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "referència externa" @@ -1621,10 +1621,10 @@ msgstr "M'agrada" msgid "Likes" msgstr "Fans" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1637,9 +1637,9 @@ msgstr "Data estimada d'inici" msgid "estimated finish date" msgstr "Data estimada de finalització" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "està tancat" @@ -1668,233 +1668,233 @@ msgstr "" msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "email" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "text extra d'invitació" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "L'usuari ja es membre del projecte" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "estatus d'història d'usuai per defecte" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "Points per defecte" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "Estatus de tasca per defecte" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "Prioritat per defecte" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "Severitat per defecte" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "Status d'incidència per defecte" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "Tipus d'incidència per defecte" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "membres" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "total de fites" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "total de punts d'història" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "activa panell de backlog" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "activa panell de kanban" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "activa panell de wiki" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "activa panell d'incidències" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "sistema de videoconferència" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "template de creació" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "es privat" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "permisos d'anònims" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "permisos d'usuaris" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "Actualitzada data" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "configuració de mòdules" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "està arxivat" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "limit de treball en progrés" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "valor" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "rol d'amo per defecte" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "opcions per defecte" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "status d'històries d'usuari" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "punts" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "status de tasques" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "status d'incidències" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "tipus d'incidències" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "prioritats" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "severitats" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "rols" @@ -2620,9 +2620,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2638,15 +2638,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "" -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "order d'històries d'usuari" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "ordre de taskboard" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "es iocaina" @@ -3268,58 +3268,58 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "ordre de backlog" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "ordre d'sprint" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "data de finalització" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "generat desde incidéncia" @@ -3428,11 +3428,11 @@ msgstr "" msgid "'project_id' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "últim a modificar" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po index f69edccb..dd4e9ac4 100644 --- a/taiga/locale/de/LC_MESSAGES/django.po +++ b/taiga/locale/de/LC_MESSAGES/django.po @@ -20,7 +20,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -225,7 +225,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Blockiertes Element" @@ -904,13 +904,13 @@ msgid "Authentication required" msgstr "Authentifizierung erforderlich" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "Name" @@ -924,12 +924,12 @@ msgid "web" msgstr "Web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "Beschreibung" @@ -963,13 +963,13 @@ msgid "comment" msgstr "Kommentar" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "Erstellungsdatum" @@ -1040,7 +1040,7 @@ msgstr "Die Nutzlast ist kein gültiges json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Das Projekt existiert nicht" @@ -1313,10 +1313,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "Besitzer" @@ -1397,17 +1397,17 @@ msgid "Project ID not matches between object and project" msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "Projekt" @@ -1420,11 +1420,11 @@ msgid "object id" msgstr "Objekt Nr." #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "Zeitpunkt der Änderung" @@ -1442,13 +1442,13 @@ msgid "is deprecated" msgstr "wurde verworfen" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "Reihenfolge" @@ -1500,29 +1500,29 @@ msgstr "Datum" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "Art" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "Werte" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "User-Story" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "Aufgabe" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "Ticket" @@ -1534,47 +1534,47 @@ msgstr "Dieser Name wird schon verwendet." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "Status" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "Betreff" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "Farbe" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "zugewiesen an" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "ist Kundenanforderung" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "ist Teamanforderung" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1710,7 +1710,7 @@ msgid "To:" msgstr "An:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "Inhalt" @@ -1755,17 +1755,17 @@ msgstr "Gewichtung" msgid "priority" msgstr "Priorität" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "Meilenstein" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "Datum der Fertigstellung" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "externe Referenz" @@ -1777,10 +1777,10 @@ msgstr "Like" msgid "Likes" msgstr "Likes" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "Slug" @@ -1793,9 +1793,9 @@ msgstr "geschätzter Starttermin" msgid "estimated finish date" msgstr "geschätzter Endtermin" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "ist geschlossen" @@ -1824,233 +1824,233 @@ msgstr "'{param}' Parameter ist ein Pflichtfeld" msgid "'project' parameter is mandatory" msgstr "Der 'project' Parameter ist ein Pflichtfeld" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "E-Mail" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "erstellt am " -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "Token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "Einladung Zusatztext " -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "Benutzerreihenfolge" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Der Benutzer ist bereits Mitglied dieses Projekts" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "voreingesteller User-Story Status " -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "voreingestellte Punkte" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "voreingestellter Aufgabenstatus" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "voreingestellte Priorität " -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "voreingestellte Gewichtung " -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "voreingestellter Ticket Status" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "voreingestellter Ticket Typ" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "Logo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "Mitglieder" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "Meilensteine Gesamt" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "Story Punkte insgesamt" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "aktives Backlog Panel" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "aktives Kanban Panel" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "aktives Wiki Panel" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "aktives Tickets Panel" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "Videokonferenzsystem" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "Zusatzdaten Videokonferenz" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "Vorlage erstellen" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "ist privat" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "Rechte für anonyme Nutzer" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "Rechte für registrierte Nutzer" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "ist gekennzeichnet" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "sucht nach Mitarbeitern" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "Hinweis für Mitarbeitersuche" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "Projekt-Transfer-Token" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "Blockierter Code" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "Aktualisierungsdatum" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "Count" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "Unterstützer letzte Woche" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "Unterstützer letzten Monat" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "Unterstützer letztes Jahr" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "Aktivitäten letzte Woche" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "Aktivitäten letzten Monat" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "Aktivitäten letztes Jahr" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "Module konfigurieren" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "ist archiviert" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "Ausführungslimit" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "Wert" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "voreingestellte Besitzerrolle" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "Vorgabe Optionen" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "User-Story Status " -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "Punkte" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "Aufgaben Status" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "Ticket Status" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "Ticket Arten" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "Prioritäten" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "Gewichtung" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "Rollen" @@ -3060,9 +3060,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -3082,15 +3082,15 @@ msgid "You don't have permissions to set this status to this task." msgstr "" "Sie haben nicht die Berechtigung, diesen Status auf diese Aufgabe zu setzen." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "User-Story Befehl " -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "Taskboard Befehl " -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "ist Iocaine" @@ -3759,62 +3759,62 @@ msgstr "Projekteigentümer " msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Sie haben nicht die Berechtigung, diesen Sprint auf diese User-Story zu " "setzen." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "Sie haben nicht die Berechtigung, diesen Status auf diese User-Story zu " "setzen." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Erstelle die User-Story #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "Rolle" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "Backlog Befehl " -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "Sprintreihenfolge" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "Endtermin" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "erzeugt von Ticket" @@ -3924,11 +3924,11 @@ msgstr "'content' Parameter ist erforderlich" msgid "'project_id' parameter is mandatory" msgstr "'project_id' Parameter ist erforderlich" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "letzte Änderung" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po index 276e9350..674cde40 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: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Taiga Dev Team \n" @@ -184,7 +184,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -743,13 +743,13 @@ msgid "Authentication required" msgstr "" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "" @@ -763,12 +763,12 @@ msgid "web" msgstr "" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "" @@ -802,13 +802,13 @@ msgid "comment" msgstr "" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "" @@ -863,7 +863,7 @@ msgstr "" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "" @@ -1136,10 +1136,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "" @@ -1218,17 +1218,17 @@ msgid "Project ID not matches between object and project" msgstr "" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "" @@ -1241,11 +1241,11 @@ msgid "object id" msgstr "" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "" @@ -1263,13 +1263,13 @@ msgid "is deprecated" msgstr "" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "" @@ -1321,29 +1321,29 @@ msgstr "" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "" @@ -1355,47 +1355,47 @@ msgstr "" msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1531,7 +1531,7 @@ msgid "To:" msgstr "" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "" @@ -1572,17 +1572,17 @@ msgstr "" msgid "priority" msgstr "" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "" @@ -1594,10 +1594,10 @@ msgstr "" msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "" @@ -1610,9 +1610,9 @@ msgstr "" msgid "estimated finish date" msgstr "" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "" @@ -1641,233 +1641,233 @@ msgstr "" msgid "'project' parameter is mandatory" msgstr "" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "" @@ -2587,9 +2587,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2605,15 +2605,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "" -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "" @@ -3217,58 +3217,58 @@ msgstr "" msgid "Stakeholder" msgstr "" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "" @@ -3377,11 +3377,11 @@ msgstr "" msgid "'project_id' parameter is mandatory" msgstr "" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "" diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po index 552d6ff8..7340cd07 100644 --- a/taiga/locale/es/LC_MESSAGES/django.po +++ b/taiga/locale/es/LC_MESSAGES/django.po @@ -17,7 +17,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -210,7 +210,7 @@ msgstr "Adjunta una imagen válida. El fichero no es una imagen o está dañada. #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Elemento bloqueado" @@ -884,13 +884,13 @@ msgid "Authentication required" msgstr "Se requiere autenticación" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nombre" @@ -904,12 +904,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "descripción" @@ -943,13 +943,13 @@ msgid "comment" msgstr "comentario" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "fecha de creación" @@ -1019,7 +1019,7 @@ msgstr "El payload no es un json válido" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "El proyecto no existe" @@ -1292,10 +1292,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "Dueño" @@ -1376,17 +1376,17 @@ msgid "Project ID not matches between object and project" msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "Proyecto" @@ -1399,11 +1399,11 @@ msgid "object id" msgstr "id de objeto" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "fecha modificada" @@ -1421,13 +1421,13 @@ msgid "is deprecated" msgstr "está desactualizado" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "orden" @@ -1479,29 +1479,29 @@ msgstr "Fecha" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "tipo" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "valores" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "historia de usuario" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "tarea" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "petición" @@ -1513,47 +1513,47 @@ msgstr "Ya existe uno con el mismo nombre." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "estado" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "asunto" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "color" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "asignado a" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "requerido por el cliente" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "requerido por el equipo" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1689,7 +1689,7 @@ msgid "To:" msgstr "A:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "contenido" @@ -1730,17 +1730,17 @@ msgstr "gravedad" msgid "priority" msgstr "prioridad" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "sprint" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "fecha de finalización" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "referencia externa" @@ -1752,10 +1752,10 @@ msgstr "Like" msgid "Likes" msgstr "Likes" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1768,9 +1768,9 @@ msgstr "fecha estimada de comienzo" msgid "estimated finish date" msgstr "fecha estimada de finalización" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "está cerrada" @@ -1801,233 +1801,233 @@ msgstr "el parámetro '{param}' es obligatório" msgid "'project' parameter is mandatory" msgstr "el parámetro 'project' es obligatório" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "email" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "creado el" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "texto extra de la invitación" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "orden del usuario" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "El usuario ya es miembro del proyecto" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "estado de historia por defecto" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "puntos por defecto" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "estado de tarea por defecto" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "prioridad por defecto" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "gravedad por defecto" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "estado de petición por defecto" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "tipo de petición por defecto" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "miembros" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "total de sprints" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "puntos de historia totales" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "panel de backlog activado" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "panel de kanban activado" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "panel de wiki activo" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "panel de peticiones activo" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "sistema de videoconferencia" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "datos extra de videoconferencia" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "creación de plantilla" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "privado" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "permisos de anónimo" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "permisos de usuario" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "es destacado" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "está buscando a gente" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "nota (buscando a gente)" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "token de transferencia de proyecto" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "código bloqueado" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "fecha y hora de actualización" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "recuento" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "fans la última semana" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "fans el último mes" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "fans el último año" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "actividad la última semana" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "actividad el último mes" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "actividad el último áño" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "configuración de modulos" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "archivado" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "limite del trabajo en progreso" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "valor" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "rol por defecto para el propietario" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "opciones por defecto" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "estatuas de historias" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "puntos" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "estatus de tareas" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "estados de petición" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "tipos de petición" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "gravedades" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "roles" @@ -2988,9 +2988,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -3006,15 +3006,15 @@ msgstr "No tienes permisos para asignar esta historia a esta tarea." msgid "You don't have permissions to set this status to this task." msgstr "No tienes permisos para asignar este estado a esta tarea." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "orden en la historia" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "orden en el taskboard" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "tiene iocaína" @@ -3749,60 +3749,60 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "No tienes permisos para asignar este sprint a esta historia de usuario." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "No tienes permisos para asignar este estado a esta historia de usuario." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Generada la historia de usuario #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "orden en el backlog" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "orden en el sprint" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "fecha de finalización" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "generada desde una petición" @@ -3912,11 +3912,11 @@ msgstr "el parámetro 'content' es obligatório" msgid "'project_id' parameter is mandatory" msgstr "el parámetro 'project_id' es obligatório" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "última modificación por" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po index f34af666..5139f702 100644 --- a/taiga/locale/fi/LC_MESSAGES/django.po +++ b/taiga/locale/fi/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -195,7 +195,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Estetty elementti" @@ -848,13 +848,13 @@ msgid "Authentication required" msgstr "" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nimi" @@ -868,12 +868,12 @@ msgid "web" msgstr "" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "kuvaus" @@ -907,13 +907,13 @@ msgid "comment" msgstr "kommentti" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "luontipvm" @@ -985,7 +985,7 @@ msgstr "The payload is not a valid json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Projektia ei löydy" @@ -1258,10 +1258,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "omistaja" @@ -1340,17 +1340,17 @@ msgid "Project ID not matches between object and project" msgstr "Projekti ID ei vastaa kohdetta ja projektia" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "projekti" @@ -1363,11 +1363,11 @@ msgid "object id" msgstr "objekti ID" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "muokkauspvm" @@ -1385,13 +1385,13 @@ msgid "is deprecated" msgstr "on poistettu" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "order" @@ -1443,29 +1443,29 @@ msgstr "" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "tyyppi" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "arvot" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "käyttäjätarina" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "tehtävä" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "pyyntö" @@ -1477,47 +1477,47 @@ msgstr "Nimi on jo olemassa" msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "viittaus" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "tila" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "aihe" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "väri" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "tekijä" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "on asiakkaan vaatimus" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "on tiimin vaatimus" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1653,7 +1653,7 @@ msgid "To:" msgstr "Kenelle:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "sisältö" @@ -1694,17 +1694,17 @@ msgstr "vakavuus" msgid "priority" msgstr "kiireellisyys" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "virstapylväs" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "loppupvm" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "ulkoinen viittaus" @@ -1716,10 +1716,10 @@ msgstr "" msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "hukka-aika" @@ -1732,9 +1732,9 @@ msgstr "arvioitu alkupvm" msgid "estimated finish date" msgstr "arvioitu loppupvm" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "on suljettu" @@ -1763,233 +1763,233 @@ msgstr "'{param}' parametri on pakollinen" msgid "'project' parameter is mandatory" msgstr "'project' parametri on pakollinen" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "sähköposti" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "luo täällä" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "tunniste" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "kutsun lisäteksti" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "käyttäjäjärjestys" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Käyttäjä on jo projektin jäsen" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "oletus Kt tila" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "oletuspisteet" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "oletus tehtävän tila" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "oletus kiireellisyys" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "oletus vakavuus" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "oletus pyynnön tila" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "oletus pyyntö tyyppi" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "jäsenet" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "virstapyväitä yhteensä" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "käyttäjätarinan yhteispisteet" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "aktiivinen odottavien paneeli" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "aktiivinen kanban-paneeli" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "aktiivinen wiki-paneeli" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "aktiivinen pyyntöpaneeli" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "videokokous järjestelmä" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "luo mallipohja" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "on yksityinen" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "vieraan oikeudet" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "käyttäjän oikeudet" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "päivityspvm" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "moduulien asetukset" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "on arkistoitu" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "työn alla olevien max" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "arvo" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "oletus omistajan rooli" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "oletus optiot" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "kt tilat" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "pisteet" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "tehtävän tilat" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "pyyntöjen tilat" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "pyyntötyypit" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "kiireellisyydet" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "vakavuudet" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "roolit" @@ -2955,9 +2955,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2973,15 +2973,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "" -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "kt järjestys" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "tehtävätaulun järjestys" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "on hidaste" @@ -3634,58 +3634,58 @@ msgstr "Tuoteomistaja" msgid "Stakeholder" msgstr "Sidosryhmä" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rooli" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "odottavien listan järjestys" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "kierros järjestys" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "loppupvm" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "luotu pyynnöstä" @@ -3794,11 +3794,11 @@ msgstr "'content' parametri on pakollinen" msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametri on pakollinen" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "viimeksi muokannut" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po index 1e218b2b..f579991b 100644 --- a/taiga/locale/fr/LC_MESSAGES/django.po +++ b/taiga/locale/fr/LC_MESSAGES/django.po @@ -23,7 +23,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -219,7 +219,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Élément bloqué" @@ -880,13 +880,13 @@ msgid "Authentication required" msgstr "Authentification requise" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nom" @@ -900,12 +900,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "description" @@ -939,13 +939,13 @@ msgid "comment" msgstr "Commentaire" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "Date de création" @@ -1015,7 +1015,7 @@ msgstr "Le payload n'est pas un json valide" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Le projet n'existe pas" @@ -1288,10 +1288,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "propriétaire" @@ -1372,17 +1372,17 @@ msgid "Project ID not matches between object and project" msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "projet" @@ -1395,11 +1395,11 @@ msgid "object id" msgstr "identifiant de l'objet" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "état modifié" @@ -1417,13 +1417,13 @@ msgid "is deprecated" msgstr "est obsolète" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "ordre" @@ -1475,29 +1475,29 @@ msgstr "Date" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "type" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "valeurs" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "histoire utilisateur" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "tâche" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "problème" @@ -1509,47 +1509,47 @@ msgstr "Un élément de même nom existe déjà" msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "réf" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "état" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "sujet" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "couleur" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "assigné à" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "est un requis client" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "est un requis de l'équipe" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1685,7 +1685,7 @@ msgid "To:" msgstr "A :" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "contenu" @@ -1726,17 +1726,17 @@ msgstr "sévérité" msgid "priority" msgstr "priorité" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "jalon" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "date de fin" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "référence externe" @@ -1748,10 +1748,10 @@ msgstr "Aimer" msgid "Likes" msgstr "Aime" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1764,9 +1764,9 @@ msgstr "date de démarrage estimée" msgid "estimated finish date" msgstr "date de fin estimée" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "est fermé" @@ -1795,233 +1795,233 @@ msgstr "'{param}' paramètre obligatoire" msgid "'project' parameter is mandatory" msgstr "'project' paramètre obligatoire" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "email" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "Créé le" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "jeton" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "Text supplémentaire de l'invitation" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "classement utilisateur" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "L'utilisateur est déjà un membre du projet" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "statut de l'HU par défaut" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "Points par défaut" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "Etat par défaut des tâches" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "Priorité par défaut" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "Sévérité par défaut" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "statut du problème par défaut" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "type de problème par défaut" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "membres" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "total des jalons" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "total des points d'histoire" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "panneau backlog actif" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "panneau kanban actif" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "panneau wiki actif" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "panneau problèmes actif" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "plateforme de vidéoconférence" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "données complémentaires pour la salle de vidéoconférence" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "Modèle de création" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "est privé" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "Permissions anonymes" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "Permission de l'utilisateur" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "est mis en avant" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "est à la recherche de main d'oeuvre" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "jeton de transfert de projet" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "code bloqué" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "date de mise à jour" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "total" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "fans la semaine dernière" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "fans le mois dernier" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "fans l'année dernière" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "activité de la semaine écoulée" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "activité du mois écoulé" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "activité de l'année écoulée" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "Configurations des modules" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "est archivé" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "limite de travail en cours" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "valeur" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "rôle par défaut du propriétaire" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "options par défaut" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "statuts des us" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "points" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "états des tâches" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "statuts des problèmes" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "types de problèmes" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "priorités" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "sévérités" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "rôles" @@ -2773,9 +2773,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2791,15 +2791,15 @@ msgstr "Vous n'avez pas la permission d'affecter ce récit à cette tâche." msgid "You don't have permissions to set this status to this task." msgstr "Vous n'avez pas la permission d'affecter ce statut à ce problème." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "ordre des us" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "order du tableau de tâches" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "est de l'iocaine" @@ -3462,60 +3462,60 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Participant" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Vous n'avez pas la permission d'affecter ce sprint à ce récit utilisateur." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "Vous n'avez pas la permission d'affecter ce statut à ce récit utilisateur." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rôle" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "order du backlog" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "ordre du sprint" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "date de fin" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "généré depuis un problème" @@ -3625,11 +3625,11 @@ msgstr "'content' paramètre obligatoire" msgid "'project_id' parameter is mandatory" msgstr "'project_id' paramètre obligatoire" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "dernier modificateur" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po index 16ff2587..b1b12bc8 100644 --- a/taiga/locale/it/LC_MESSAGES/django.po +++ b/taiga/locale/it/LC_MESSAGES/django.po @@ -15,7 +15,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -205,7 +205,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -926,13 +926,13 @@ msgid "Authentication required" msgstr "E' richiesta l'autenticazione" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nome" @@ -946,12 +946,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "descrizione" @@ -985,13 +985,13 @@ msgid "comment" msgstr "Commento" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "data creata" @@ -1067,7 +1067,7 @@ msgstr "Il carico non è un json valido" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Il progetto non esiste" @@ -1340,10 +1340,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "proprietario" @@ -1422,17 +1422,17 @@ msgid "Project ID not matches between object and project" msgstr "L'ID di progetto non corrisponde tra oggetto e progetto" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "progetto" @@ -1445,11 +1445,11 @@ msgid "object id" msgstr "ID dell'oggetto" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "data modificata" @@ -1467,13 +1467,13 @@ msgid "is deprecated" msgstr "non approvato" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "ordine" @@ -1525,29 +1525,29 @@ msgstr "Data" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "tipo" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "valori" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "storia utente" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "compito" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "problema" @@ -1559,47 +1559,47 @@ msgstr "Ne esiste già un altro con lo stesso nome" msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "referenza" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "stato" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "soggeto" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "colore" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "assegnato a" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "é un requisito del cliente " -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "é una richiesta del team" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1735,7 +1735,7 @@ msgid "To:" msgstr "A:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "contenuto" @@ -1776,17 +1776,17 @@ msgstr "criticità" msgid "priority" msgstr "priorità" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "tappa" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "data di conclusione" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "referenza esterna" @@ -1798,10 +1798,10 @@ msgstr "Like" msgid "Likes" msgstr "Piaciuto" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "lumaca" @@ -1814,9 +1814,9 @@ msgstr "data stimata di inizio" msgid "estimated finish date" msgstr "data stimata di fine" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "è concluso" @@ -1846,233 +1846,233 @@ msgstr "il parametro '{param}' è obbligatorio" msgid "'project' parameter is mandatory" msgstr "il parametro 'project' è obbligatorio" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "email" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "creato a " -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "testo ulteriore per l'invito" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "ordine dell'utente" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "L'utente è già membro del progetto" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "stati predefiniti per le storie utente" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "punti predefiniti" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "stati predefiniti del compito" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "priorità predefinita" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "criticità predefinita" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "stato predefinito del problema" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "tipologia predefinita del problema" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "membri" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "tappe totali" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "punti totali della storia" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "pannello di backlog attivo" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "pannello kanban attivo" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "pannello wiki attivo" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "pannello dei problemi attivo" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "sistema di videoconferenza" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "ulteriori dati di videoconferenza " -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "creazione del template" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "è privato" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "permessi anonimi" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "permessi dell'utente" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "in vetrina" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "sta cercando persone" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "note sulla ricerca delle persone " -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "tempo e data aggiornati" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "conta" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "fans nella settimana" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "fans nel mese" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "fans nell'anno" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "attività nella settimana" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "attività nel mese" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "attività nell'anno" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "configurazione dei moduli" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "è archivitato" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "limite dei lavori in corso" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "valore" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "ruolo proprietario predefinito" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "opzioni predefinite " -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "stati della storia utente" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "punti" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "stati del compito" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "stati del probema" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "tipologie del problema" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "priorità" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "criticità " -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "ruoli" @@ -3178,9 +3178,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -3197,15 +3197,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "Non hai i permessi per aggiungere questo stato a questo compito." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "ordine della storia utente" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "ordine del pannello dei compiti" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "è sotto aspirina" @@ -3890,59 +3890,59 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Non hai i permessi per aggiungere questo sprint a questa storia utente." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "Non hai i permessi per aggiungere questo stato a questa storia utente." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Stiamo generando la storia utente #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "ruolo" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "ordine del backlog" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "ordine dello sprint" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "data di termine" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "generato da un problema" @@ -4051,11 +4051,11 @@ msgstr "il parametro 'contenuto' è obbligatorio" msgid "'project_id' parameter is mandatory" msgstr "Il parametro 'ID progetto' è obbligatorio" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "ultima modificatore" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/nb/LC_MESSAGES/django.po b/taiga/locale/nb/LC_MESSAGES/django.po index cff071a0..0524d9ef 100644 --- a/taiga/locale/nb/LC_MESSAGES/django.po +++ b/taiga/locale/nb/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Norwegian Bokmål (http://www.transifex.com/taiga-agile-llc/" @@ -194,7 +194,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Blokkert element" @@ -764,13 +764,13 @@ msgid "Authentication required" msgstr "Autentisering kreves" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "navn" @@ -784,12 +784,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "beskrivelse" @@ -823,13 +823,13 @@ msgid "comment" msgstr "kommentar" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "opprettet dato" @@ -900,7 +900,7 @@ msgstr "Payloaden er ikke gyldig json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Prosjektet eksisterer ikke" @@ -1173,10 +1173,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "eier" @@ -1257,17 +1257,17 @@ msgid "Project ID not matches between object and project" msgstr "Prosjekt ID matcher ikke mellom objekt og prosjekt" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "prosjekt" @@ -1280,11 +1280,11 @@ msgid "object id" msgstr "objektid" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "redigeringsdato" @@ -1302,13 +1302,13 @@ msgid "is deprecated" msgstr "er foreldet" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "rekkefølge" @@ -1360,29 +1360,29 @@ msgstr "Dato" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "type" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "verdier" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "brukerhistorie" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "oppgave" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "hendelse" @@ -1394,47 +1394,47 @@ msgstr "Det finnes allerede en med samme navn." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "status" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "subjekt" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "farge" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "tildelt til" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "Er klientkrav" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "Er team behov" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1570,7 +1570,7 @@ msgid "To:" msgstr "Til: " #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "innhold" @@ -1614,17 +1614,17 @@ msgstr "alvorlighetsgrad" msgid "priority" msgstr "prioritet" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "milepæl" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "Sluttdato" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "ekstern referanse" @@ -1636,10 +1636,10 @@ msgstr "Liker" msgid "Likes" msgstr "Liker" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1652,9 +1652,9 @@ msgstr "anslått startdato" msgid "estimated finish date" msgstr "anslått sluttdato" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "er lukket" @@ -1683,233 +1683,233 @@ msgstr "'{param}' parameter er obligatorisk" msgid "'project' parameter is mandatory" msgstr "'project' parameter er obligatorisk" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "epost" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "opprett ved" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "invitasjon ekstra tekst" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "bruker rekkefølge" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Denne brukeren er allerede medlem av prosjektet" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "standard brukerhistoriestatuser" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "standardpoeng" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "standard oppgavestatuser" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "standard prioriteter" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "standard alvorlighetsgrad" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "standard hendelsesstatuser" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "standard hendelsestyper" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "medlemmer" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "total av milepæler" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "total historiepoeng" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "aktivt backlogpanel" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "aktivt kanbanpanel" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "aktivt wikipanel" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "aktivt hendelsespanel" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "videokonferansesystem" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "videokonferanse ekstra data" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "skapelsesmal" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "er privat" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "anonymes rettigheter" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "brukerrettigheter" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "er omtalt" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "er søker etter folk" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "søker etter folk notat" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "prosjektflyttingstoken" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "blokkert kode" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "oppdatert dato tid" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "antall" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "fans forrige uke" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "fans forrige måned" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "fans forrige år" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "aktivitet forrige uke" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "aktivitet forrige måned" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "aktivitet forrige år" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "modulkonfigurasjon" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "er arkivert" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "arbeid som pågår grense" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "verdi" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "standard eiers rolle" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "standardvalg" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "bh statuser" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "poeng" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "oppgavestatuser" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "hendelsesstatuser" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "hendelsestyper" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "prioriteter" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "alvorlighetsgrader" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "roller" @@ -2636,9 +2636,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2655,15 +2655,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "Du har ikke tillatelse til å sette denne statusen til denne oppgaven." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "BH rekkefølge" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "Oppgavetavle rekkefølge" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "Er Iocaine" @@ -3267,60 +3267,60 @@ msgstr "Produkteier" msgid "Stakeholder" msgstr "Interessent" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Du har ikke tillatelse til å sette denne sprinten til denne brukerhistorien." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "Du har ikke tillatelse til å sette denne statusen til denne brukerhistorien." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Genererer brukerhistorien #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rolle" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "backlog rekkefølge" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "sprint rekkefølge" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "Sluttdato" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "" @@ -3429,11 +3429,11 @@ msgstr "'content' parameteren er obligatorisk" msgid "'project_id' parameter is mandatory" msgstr "'project_id' parameteren er obligatorisk" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "sist endret av" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po index 3962f37c..0a0cd5ad 100644 --- a/taiga/locale/nl/LC_MESSAGES/django.po +++ b/taiga/locale/nl/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -203,7 +203,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -794,13 +794,13 @@ msgid "Authentication required" msgstr "" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "naam" @@ -814,12 +814,12 @@ msgid "web" msgstr "" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "omschrijving" @@ -853,13 +853,13 @@ msgid "comment" msgstr "commentaar" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "aanmaakdatum" @@ -930,7 +930,7 @@ msgstr "De payload is geen geldige json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Het project bestaat niet" @@ -1203,10 +1203,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "eigenaar" @@ -1285,17 +1285,17 @@ msgid "Project ID not matches between object and project" msgstr "Project ID van object is niet gelijk aan die van het project" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "project" @@ -1308,11 +1308,11 @@ msgid "object id" msgstr "object id" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "gemodifieerde datum" @@ -1330,13 +1330,13 @@ msgid "is deprecated" msgstr "is verouderd" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "volgorde" @@ -1388,29 +1388,29 @@ msgstr "" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "type" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "waarden" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "user story" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "taak" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "issue" @@ -1422,47 +1422,47 @@ msgstr "Er bestaat er al één met dezelfde naam." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "status" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "onderwerp" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "kleur" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "toegewezen aan" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "is requirement van de klant" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "is requirement van het team" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1598,7 +1598,7 @@ msgid "To:" msgstr "Naar:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "inhoud" @@ -1641,17 +1641,17 @@ msgstr "erstniveau" msgid "priority" msgstr "prioriteit" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "milestone" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "datum van afwerking" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "externe referentie" @@ -1663,10 +1663,10 @@ msgstr "Vind ik leuk" msgid "Likes" msgstr "Personen die dit leuk vinden" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1679,9 +1679,9 @@ msgstr "geschatte start datum" msgid "estimated finish date" msgstr "geschatte datum van afwerking" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "is gesloten" @@ -1710,233 +1710,233 @@ msgstr "'{param}' parameter is verplicht" msgid "'project' parameter is mandatory" msgstr "'project' parameter is verplicht" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "aangemaakt op" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "uitnodiging extra text" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "gebruiker volgorde" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "The gebruikers is al lid van het project" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "standaard US status" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "standaard punten" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "default taak status" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "standaard prioriteit" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "standaard ernstniveau" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "standaard issue status" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "standaard issue type" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "leden" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "totaal van de milestones" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "totaal story points" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "actief backlog paneel" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "actief kanban paneel" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "actief wiki paneel" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "actief issues paneel" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "videoconference systeem" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "aanmaak template" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "is privé" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "anonieme toestemmingen" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "gebruikers toestemmingen" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "gewijzigde datum en tijd" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "module config" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "is gearchiveerd" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "work in progress limiet" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "waarde" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "standaard rol eigenaar" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "standaard instellingen" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "us statussen" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "punten" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "taak statussen" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "issue statussen" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "issue types" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "prioriteiten" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "ernstniveaus" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "rollen" @@ -2686,9 +2686,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2704,15 +2704,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "" -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "us volgorde" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "takenbord volgorde" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "is iocaine" @@ -3340,58 +3340,58 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "backlog volgorde" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "sprint volgorde" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "afwerkdatum" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "gegenereerd van issue" @@ -3500,11 +3500,11 @@ msgstr "'inhoud' parameter is verplicht" msgid "'project_id' parameter is mandatory" msgstr "'project_id' parameter is verplicht" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "gebruiker met laatste wijziging" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po index 38f5cfc9..b40c46b2 100644 --- a/taiga/locale/pl/LC_MESSAGES/django.po +++ b/taiga/locale/pl/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -197,7 +197,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -862,13 +862,13 @@ msgid "Authentication required" msgstr "" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "nazwa" @@ -882,12 +882,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "opis" @@ -921,13 +921,13 @@ msgid "comment" msgstr "komentarz" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "data utworzenia" @@ -998,7 +998,7 @@ msgstr "Źródło nie jest prawidłowym plikiem json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Projekt nie istnieje" @@ -1271,10 +1271,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "właściciel" @@ -1353,17 +1353,17 @@ msgid "Project ID not matches between object and project" msgstr "ID nie pasuje pomiędzy obiektem a projektem" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "projekt" @@ -1376,11 +1376,11 @@ msgid "object id" msgstr "id obiektu" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "data modyfikacji" @@ -1398,13 +1398,13 @@ msgid "is deprecated" msgstr "jest przestarzałe" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "kolejność" @@ -1456,29 +1456,29 @@ msgstr "" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "typ" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "wartości" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "historyjka użytkownika" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "zadanie" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "zgłoszenie" @@ -1490,47 +1490,47 @@ msgstr "Już istnieje jeden z taką nazwą." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "status" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "temat" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "kolor" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "przypisane do" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "wymaganie klienta" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "wymaganie zespołu" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1666,7 +1666,7 @@ msgid "To:" msgstr "Do:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "zawartość" @@ -1707,17 +1707,17 @@ msgstr "ważność" msgid "priority" msgstr "priorytet" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "kamień milowy" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "data zakończenia" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "źródło zgłoszenia" @@ -1729,10 +1729,10 @@ msgstr "" msgid "Likes" msgstr "" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1745,9 +1745,9 @@ msgstr "szacowana data rozpoczecia" msgid "estimated finish date" msgstr "szacowana data zakończenia" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "jest zamknięte" @@ -1776,233 +1776,233 @@ msgstr "'{param}' parametr jest obowiązkowy" msgid "'project' parameter is mandatory" msgstr "'project' parametr jest obowiązkowy" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "e-mail" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "utwórz na" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "dodatkowy tekst w zaproszeniu" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "kolejność użytkowników" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Użytkownik już jest członkiem tego projektu" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "domyślny status dla HU" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "domyślne punkty" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "domyślny status dla zadania" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "domyślny priorytet" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "domyślna ważność" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "domyślny status dla zgłoszenia" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "domyślny typ dla zgłoszenia" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "członkowie" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "wszystkich kamieni milowych" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "wszystkich punktów " -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "aktywny panel backlog" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "aktywny panel Kanban" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "aktywny panel Wiki" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "aktywny panel zgłoszeń " -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "system wideokonferencji" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "dodatkowe dane dla wideokonferencji" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "szablon " -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "jest prywatna" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "uprawnienia anonimowych" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "uprawnienia użytkownika" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "data aktualizacji" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "ilość" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "konfiguracja modułów" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "zarchiwizowane" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "limit postępu prac" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "wartość" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "domyśla rola właściciela" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "domyślne opcje" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "statusy HU" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "pinkty" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "statusy zadań" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "statusy zgłoszeń" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "typy zgłoszeń" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "priorytety" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "ważność" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "role" @@ -2983,9 +2983,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -3002,15 +3002,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "Nie masz uprawnień do ustawiania statusu dla tego zadania" -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "kolejność HU" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "Kolejność tablicy zadań" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "Iokaina" @@ -3665,60 +3665,60 @@ msgstr "Właściciel produktu" msgid "Stakeholder" msgstr "Interesariusz" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Nie masz uprawnień do ustawiania sprintu dla tej historyjki użytkownika." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "Nie masz uprawnień do ustawiania statusu do tej historyjki użytkownika." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rola" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "Kolejność backlogu" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "kolejność sprintu" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "data zakończenia" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "wygenerowane ze zgłoszenia" @@ -3827,11 +3827,11 @@ msgstr "Parametr 'zawartość' jest wymagany" msgid "'project_id' parameter is mandatory" msgstr "Parametr 'id_projektu' jest wymagany" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "ostatnio zmodyfikowane przez" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po index aaa0f7bc..6014decf 100644 --- a/taiga/locale/pt_BR/LC_MESSAGES/django.po +++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po @@ -22,7 +22,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/taiga-agile-llc/" @@ -207,7 +207,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Elemento bloqeado" @@ -893,13 +893,13 @@ msgid "Authentication required" msgstr "Autenticação necessária" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "Nome" @@ -913,12 +913,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "descrição" @@ -952,13 +952,13 @@ msgid "comment" msgstr "comentário" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "data de criação" @@ -1029,7 +1029,7 @@ msgstr "A carga não é um json válido" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "O projeto não existe" @@ -1302,10 +1302,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "dono" @@ -1386,17 +1386,17 @@ msgid "Project ID not matches between object and project" msgstr "ID do projeto não combina entre objeto e projeto" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "projeto" @@ -1409,11 +1409,11 @@ msgid "object id" msgstr "identidade de objeto" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "data modificação" @@ -1431,13 +1431,13 @@ msgid "is deprecated" msgstr "está obsoleto" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "ordem" @@ -1489,29 +1489,29 @@ msgstr "Data" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "Tipo" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "valores" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "história de usuário" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "tarefa" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "problema" @@ -1523,47 +1523,47 @@ msgstr "Já existe um com o mesmo nome." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "status" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "assunto" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "cor" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "assinado a" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "É requerimento do cliente" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "É requerimento do time" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1699,7 +1699,7 @@ msgid "To:" msgstr "Para:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "conteúdo" @@ -1741,17 +1741,17 @@ msgstr "severidade" msgid "priority" msgstr "prioridade" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "marco de progresso" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "data de término" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "referência externa" @@ -1763,10 +1763,10 @@ msgstr "Curtir" msgid "Likes" msgstr "Curtidas" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slug" @@ -1779,9 +1779,9 @@ msgstr "data de início estimada" msgid "estimated finish date" msgstr "data de encerramento estimada" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "está fechado" @@ -1810,233 +1810,233 @@ msgstr "'{param}' parametro é mandatório" msgid "'project' parameter is mandatory" msgstr "'project' parametro é mandatório" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "email" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "criado em" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "token" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "texto extra de convite" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "ordem de usuário" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "O usuário já é membro do projeto" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "status de US padrão" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "pontos padrão" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "status padrão de tarefa" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "prioridade padrão" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "severidade padrão" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "status padrão de problema" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "tipo padrão de problema" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "logotipo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "membros" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "total de marcos de progresso" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "pontos totais de US" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "painel de backlog ativo" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "painel de kanban ativo" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "painel de wiki ativo" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "painel de problemas ativo" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "sistema de vídeo conferência" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "informação extra de vídeo conferência" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "template de criação" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "é privado" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "permissão anônima" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "permissão de usuário" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "é destaque" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "está procurando colaboradores" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "data de atualização" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "contagem" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "atividades da última semana" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "atividades do último mês" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "atividades do último ano" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "configurações de módulos" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "está arquivado" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "trabalho no limite de progresso" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "valor" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "função padrão para dono " -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "opções padrão" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "status de US" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "pontos" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "status de tarefa" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "status de problemas" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "tipos de problema" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "prioridades" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "severidades" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "funções" @@ -3001,9 +3001,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -3021,15 +3021,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "Você não tem permissão para colocar esse status para essa tarefa." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "ordenar por US" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "ordenar por quadro de tarefa" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "é Iocaine" @@ -3695,62 +3695,62 @@ msgstr "Product Owner" msgid "Stakeholder" msgstr "Stakeholder" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Você não tem permissão para colocar esse sprint para essa história de " "usuário." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "Você não tem permissão para colocar esse status para essa história de " "usuário." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Gerando a história de usuário #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "função" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "ordem do backlog" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "ordem do sprint" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "data de término" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "Gerado do problema" @@ -3860,11 +3860,11 @@ msgstr "parâmetro 'conteúdo' é mandatório" msgid "'project_id' parameter is mandatory" msgstr "parametro 'project_id' é mandatório" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "último modificador" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po index a70669de..e4d316a6 100644 --- a/taiga/locale/ru/LC_MESSAGES/django.po +++ b/taiga/locale/ru/LC_MESSAGES/django.po @@ -16,7 +16,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -208,7 +208,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Заблокированный элемент" @@ -884,13 +884,13 @@ msgid "Authentication required" msgstr "Необходима аутентификация" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "имя" @@ -904,12 +904,12 @@ msgid "web" msgstr "веб" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "описание" @@ -943,13 +943,13 @@ msgid "comment" msgstr "комментарий" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "дата создания" @@ -1020,7 +1020,7 @@ msgstr "Нагрузочный файл не является правильны #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Проект не существует" @@ -1293,10 +1293,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "владелец" @@ -1377,17 +1377,17 @@ msgid "Project ID not matches between object and project" msgstr "Идентификатор проекта не подходит к этому объекту" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "проект" @@ -1400,11 +1400,11 @@ msgid "object id" msgstr "идентификатор объекта" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "изменённая дата" @@ -1422,13 +1422,13 @@ msgid "is deprecated" msgstr "устаревшее" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "порядок" @@ -1480,29 +1480,29 @@ msgstr "Дата" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "тип" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "значения" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "пользовательская история" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "задача" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "запрос" @@ -1514,47 +1514,47 @@ msgstr "Это имя уже используется." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "Ссылка" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "cтатус" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "тема" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "цвет" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "назначено" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "является требованием клиента" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "является требованием команды" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1690,7 +1690,7 @@ msgid "To:" msgstr "Кому:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "содержимое" @@ -1735,17 +1735,17 @@ msgstr "важность" msgid "priority" msgstr "приоритет" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "веха" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "дата завершения" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "внешняя ссылка" @@ -1757,10 +1757,10 @@ msgstr "Лайк" msgid "Likes" msgstr "Лайки" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "ссылочное имя" @@ -1773,9 +1773,9 @@ msgstr "предполагаемая дата начала" msgid "estimated finish date" msgstr "предполагаемая дата завершения" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "закрыто" @@ -1806,233 +1806,233 @@ msgstr "параметр '{param}' является обязательным" msgid "'project' parameter is mandatory" msgstr "параметр 'project' является обязательным" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "электронная почта" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "создано" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "идентификатор" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "дополнительный текст к приглашению" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "порядок пользователей" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Этот пользователем уже является участником проекта" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "статусы ПИ по умолчанию" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "очки по умолчанию" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "статус задачи по умолчанию" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "приоритет по умолчанию" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "важность по умолчанию" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "статус запроса по умолчанию" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "тип запроса по умолчанию" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "лготип" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "участники" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "общее количество вех" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "очки истории" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "активная панель списка задач" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "активная панель kanban" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "активная wiki-панель" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "панель активных запросов" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "система видеоконференций" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "дополнительные данные системы видеоконференций" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "шаблон для создания" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "личное" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "права анонимов" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "права пользователя" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "особенность" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "ищут людей" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "ищем замечания людей" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "токен передачи проекта" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "заблокированный код" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "дата и время обновления" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "количество" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "фанатов на прошлой недели " -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "фанатов в прошлом месяце" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "фанатов в прошлом году" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "активность за неделю" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "активность за месяц" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "активность за год" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "конфигурация модулей" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "архивировано" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "ограничение на активную работу" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "значение" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "роль владельца по умолчанию" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "параметры по умолчанию" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "статусы ПИ" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "очки" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "статусы задач" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "статусы запросов" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "типы запросов" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "приоритеты" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "степени важности" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "роли" @@ -3000,9 +3000,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -3019,15 +3019,15 @@ msgstr "" msgid "You don't have permissions to set this status to this task." msgstr "У вас нет прав, чтобы установить этот статус для этой задачи." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "порядок ПИ" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "порядок панели задач" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "- иокаин" @@ -3762,60 +3762,60 @@ msgstr "Владелец продукта" msgid "Stakeholder" msgstr "Заинтересованная сторона" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "У вас нет прав чтобы установить спринт для этой пользовательской истории." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "У вас нет прав чтобы установить статус для этой пользовательской истории." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Генерируется пользовательская история #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "роль" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "порядок списка задач" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "порядок спринтов" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "дата окончания" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "создано из запроса" @@ -3926,11 +3926,11 @@ msgstr "параметр 'content' является обязательным" msgid "'project_id' parameter is mandatory" msgstr "параметр 'project_id' является обязательным" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "последний отредактировавший" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po index b8f0cec5..09cbdb49 100644 --- a/taiga/locale/sv/LC_MESSAGES/django.po +++ b/taiga/locale/sv/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -196,7 +196,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -761,13 +761,13 @@ msgid "Authentication required" msgstr "Verifiering krävs" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "namn" @@ -781,12 +781,12 @@ msgid "web" msgstr "Internet" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "beskrivning" @@ -820,13 +820,13 @@ msgid "comment" msgstr "kommentera" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "skapad datum" @@ -881,7 +881,7 @@ msgstr "Datasträngen är inte korrekt json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Projektet existerar inte" @@ -1154,10 +1154,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "ägare" @@ -1236,17 +1236,17 @@ msgid "Project ID not matches between object and project" msgstr "Projekt-ID stämmer inte mellan objekt och projekt" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "projekt" @@ -1259,11 +1259,11 @@ msgid "object id" msgstr "objekt-ID" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "ändrad datum" @@ -1281,13 +1281,13 @@ msgid "is deprecated" msgstr "undviks" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "sortera" @@ -1339,29 +1339,29 @@ msgstr "Datum" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "typ" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "värden" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "Användarhistorie" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "uppgift" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "Ärende" @@ -1373,47 +1373,47 @@ msgstr "Existerar redan med samma namn. " msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "status" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "titel" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "färg" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "Tilldelad till" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "är ett beställarkrav" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "är ett krav från arbetsgruppen" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1549,7 +1549,7 @@ msgid "To:" msgstr "Till:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "innehåll" @@ -1590,17 +1590,17 @@ msgstr "Allvarsgrad" msgid "priority" msgstr "prioritet" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "milstolpe" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "färdig datum" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "extern referens" @@ -1612,10 +1612,10 @@ msgstr "Gillar" msgid "Likes" msgstr "Gillar" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "slugg" @@ -1628,9 +1628,9 @@ msgstr "Beräknad startdatum" msgid "estimated finish date" msgstr "Beräknad slutdato" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "är stängd" @@ -1659,233 +1659,233 @@ msgstr "'{param}' parameter är obligatoriskt" msgid "'project' parameter is mandatory" msgstr "'project' parameter är obligatoriskt" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "e-post" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "skapa som" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "textsträng" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "Invitation - extra text" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "användarorder" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Användaren är redan medlem i projekt" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "standard US-poäng" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "standardpoäng" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "standard status för uppgift" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "standard prioritet" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "standard allvarsgrad" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "standard status för ärende" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "standard typ för ärende" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "medlemmar" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "totalt antal milstolpar" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "totalt antal historiepoäng" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "aktivt panel för inkorg" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "aktiv kanban-panel" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "aktiv wiki-panel" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "aktiv panel för ärenden" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "videokonferensssystem" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "videokonferens - extra data" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "mall skapas" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "är privat" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "anonyma rättigheter" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "användarbehörigheter" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "uppdaterad dato och tid" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "räkna" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "konfigurera moduler" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "är arkiverad" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "begränsad arbete pågår" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "värde" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "ägarens standardroll" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "standard val" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "US statuser" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "poäng" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "statuser för uppgifter" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "status för ärenden" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "ärendentyper" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "prioriteter" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "allvarsgrad" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "roller" @@ -2605,9 +2605,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2623,15 +2623,15 @@ msgstr "Du har inte behörighet att sätta använderhistorien till en uppgift." msgid "You don't have permissions to set this status to this task." msgstr "Du har inte behörighet att sätta status till en uppgift. " -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "sortera US" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "Sortera uppgiftstavlan" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "är Iocaine" @@ -3245,61 +3245,61 @@ msgstr "Produktägare" msgid "Stakeholder" msgstr "Intressent" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "" "Du har inte behörighet för att lägga sprinten till den här användarhistorien" -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "" "Du har inte behörighet till att sätta den här statusen till " "användarhistorien." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "Skapar användarhistorie #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "roll" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "sortera inkorgen" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "sortera sprintar" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "färdig datum" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "skapad från ärende" @@ -3408,11 +3408,11 @@ msgstr "'content' parametern är obligatoriskt" msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametern är obligatoriskt" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "senastste ändring" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po index 379339fc..d070f778 100644 --- a/taiga/locale/tr/LC_MESSAGES/django.po +++ b/taiga/locale/tr/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/" @@ -204,7 +204,7 @@ msgstr "" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "Engellenmiş nesne" @@ -856,13 +856,13 @@ msgid "Authentication required" msgstr "Kimlik doğrulama gerekli" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "isim" @@ -876,12 +876,12 @@ msgid "web" msgstr "web" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "tanı" @@ -915,13 +915,13 @@ msgid "comment" msgstr "yorum" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "oluşturma tarihi" @@ -990,7 +990,7 @@ msgstr "" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "Proje mevcut değil." @@ -1263,10 +1263,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "sahip" @@ -1345,17 +1345,17 @@ msgid "Project ID not matches between object and project" msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "proje" @@ -1368,11 +1368,11 @@ msgid "object id" msgstr "nesne id" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "düzenleme tarihi" @@ -1390,13 +1390,13 @@ msgid "is deprecated" msgstr "kaldırıldı" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "sıra" @@ -1448,29 +1448,29 @@ msgstr "Tarih" msgid "Url" msgstr "Url" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "tip" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "değerler" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "kullanıcı hikayesi" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "görev" -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "talep" @@ -1482,47 +1482,47 @@ msgstr "Aynı isimler bir tane daha mevcut." msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "durum" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "konu" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "renk" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "atanmış" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "istemci gereksinimi" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "takım gereksinimi" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1658,7 +1658,7 @@ msgid "To:" msgstr "Kime:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "içerik" @@ -1699,17 +1699,17 @@ msgstr "önem derecesi" msgid "priority" msgstr "öncelik" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "aşama" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "bitirme tarihi" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "dış referans" @@ -1721,10 +1721,10 @@ msgstr "Beğen" msgid "Likes" msgstr "Beğeniler" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "satır" @@ -1737,9 +1737,9 @@ msgstr "yaklaşık başlama tarihi" msgid "estimated finish date" msgstr "yaklaşık bitiş tarihi" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "kapatılmış" @@ -1768,233 +1768,233 @@ msgstr "'{param}' parametresi zorunlu" msgid "'project' parameter is mandatory" msgstr "'proje' parametresi zorunlu" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "e-posta" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "kupon" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "Davetiye ekstra metni" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "kullanıcı sırası" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "Kullanıcı zaten projenin üyesi" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "varsayılan KH durumu" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "varsayılan puanlar" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "varsayılan görev durumu" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "varsayılan öncelik" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "varsayılan önem derecesi" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "varsayılan talep durumu" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "varsayılan talep tipi" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "logo" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "üyeler" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "aşamaların toplamı" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "toplam hikaye puanı" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "aktif birikmiş iler paneli" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "aktif kanban paneli" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "aktif wiki paneli" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "aktif talep paneli" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "video konferans sistemi" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "videokonferans ekstra verisi" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "oluşturma şablonu" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "gizli" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "anonim izinler" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "kullanıcı izinleri" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr "vitrinde" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "insan arıyor" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "engellenmiş kod" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "yükleme tarih-saati" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "sayı" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "geçen hafta fanları" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "geçen ayın fanları" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "geçen yılın fanları" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "geçen haftanın aktiviteleri" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "geçen ayın aktiviteleri" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "geçen yılın aktiviteleri" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "modül ayarları" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "arşivlenmiş" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "değer" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "varsayılan sahip rolü" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "varsayılan ayarlar" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "kh durumları" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "puanlar" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "görev durumları" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "talep durumları" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "talep tipleri" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "öncelikler" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "önem durumları" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "roller" @@ -2780,9 +2780,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2798,15 +2798,15 @@ msgstr "Bu görev için kullanıcı hikayesi ayarlama izniniz yok." msgid "You don't have permissions to set this status to this task." msgstr "Bu görev için bu durumu ayarlama izniniz yok." -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "kh sırası" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "görev panosu sırası" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "baldıran zehri" @@ -3422,58 +3422,58 @@ msgstr "Ürün Sahibi" msgid "Stakeholder" msgstr "Paydaş" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "Bu kullanıcı hikayesine bu sprinti ayarlama izniniz yok." -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "Bu kullanıcı hikayesine bu durumu ayarlama yetkiniz yok." -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "rol" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "birikmiş işler sırası" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "sprint sırası" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "bitiş tarihi" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "talepden oluştur" @@ -3582,11 +3582,11 @@ msgstr "'content' parametresi zorunlu" msgid "'project_id' parameter is mandatory" msgstr "'project_id' parametresi zorunlu" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "son düzenleyen" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po index 9a658433..fcbeb9ea 100644 --- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po +++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: taiga-back\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-09-20 12:49+0200\n" +"POT-Creation-Date: 2016-09-28 10:29+0200\n" "PO-Revision-Date: 2016-09-20 10:50+0000\n" "Last-Translator: Taiga Dev Team \n" "Language-Team: Chinese Traditional (http://www.transifex.com/taiga-agile-llc/" @@ -190,7 +190,7 @@ msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞" #: taiga/projects/epics/api.py:213 taiga/projects/epics/api.py:292 #: taiga/projects/issues/api.py:238 taiga/projects/mixins/ordering.py:59 #: taiga/projects/tasks/api.py:261 taiga/projects/tasks/api.py:287 -#: taiga/projects/userstories/api.py:335 taiga/projects/userstories/api.py:387 +#: taiga/projects/userstories/api.py:340 taiga/projects/userstories/api.py:392 #: taiga/webhooks/api.py:71 msgid "Blocked element" msgstr "" @@ -842,13 +842,13 @@ msgid "Authentication required" msgstr "要求取得授權" #: taiga/external_apps/models.py:35 -#: taiga/projects/custom_attributes/models.py:35 -#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:144 -#: taiga/projects/models.py:511 taiga/projects/models.py:544 -#: taiga/projects/models.py:580 taiga/projects/models.py:602 -#: taiga/projects/models.py:636 taiga/projects/models.py:656 -#: taiga/projects/models.py:676 taiga/projects/models.py:708 -#: taiga/projects/models.py:728 taiga/users/admin.py:54 +#: taiga/projects/custom_attributes/models.py:36 +#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:145 +#: taiga/projects/models.py:512 taiga/projects/models.py:545 +#: taiga/projects/models.py:581 taiga/projects/models.py:603 +#: taiga/projects/models.py:637 taiga/projects/models.py:657 +#: taiga/projects/models.py:677 taiga/projects/models.py:709 +#: taiga/projects/models.py:729 taiga/users/admin.py:54 #: taiga/users/models.py:292 taiga/webhooks/models.py:29 msgid "name" msgstr "姓名" @@ -862,12 +862,12 @@ msgid "web" msgstr "網頁" #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:61 -#: taiga/projects/custom_attributes/models.py:36 -#: taiga/projects/epics/models.py:54 +#: taiga/projects/custom_attributes/models.py:37 +#: taiga/projects/epics/models.py:55 #: taiga/projects/history/templatetags/functions.py:25 -#: taiga/projects/issues/models.py:60 taiga/projects/models.py:148 -#: taiga/projects/models.py:732 taiga/projects/tasks/models.py:61 -#: taiga/projects/userstories/models.py:94 +#: taiga/projects/issues/models.py:60 taiga/projects/models.py:149 +#: taiga/projects/models.py:733 taiga/projects/tasks/models.py:62 +#: taiga/projects/userstories/models.py:95 msgid "description" msgstr "描述" @@ -901,13 +901,13 @@ msgid "comment" msgstr "評論" #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 -#: taiga/projects/custom_attributes/models.py:45 -#: taiga/projects/epics/models.py:47 taiga/projects/issues/models.py:52 +#: taiga/projects/custom_attributes/models.py:46 +#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:49 -#: taiga/projects/models.py:155 taiga/projects/models.py:736 -#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:47 -#: taiga/projects/userstories/models.py:86 taiga/projects/votes/models.py:54 -#: taiga/projects/wiki/models.py:43 taiga/userstorage/models.py:29 +#: taiga/projects/models.py:156 taiga/projects/models.py:737 +#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 +#: taiga/projects/userstories/models.py:87 taiga/projects/votes/models.py:54 +#: taiga/projects/wiki/models.py:44 taiga/userstorage/models.py:29 msgid "created date" msgstr "創建日期" @@ -975,7 +975,7 @@ msgstr "載荷為無效json" #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:152 #: taiga/projects/issues/api.py:138 taiga/projects/tasks/api.py:200 -#: taiga/projects/userstories/api.py:268 +#: taiga/projects/userstories/api.py:273 msgid "The project doesn't exist" msgstr "專案不存在" @@ -1248,10 +1248,10 @@ msgid "Fans" msgstr "" #: taiga/projects/admin.py:145 taiga/projects/attachments/models.py:39 -#: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:37 -#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:160 -#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:38 -#: taiga/projects/userstories/models.py:68 taiga/projects/wiki/models.py:39 +#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 +#: taiga/projects/milestones/models.py:43 taiga/projects/models.py:161 +#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 +#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/users/admin.py:69 taiga/userstorage/models.py:27 msgid "owner" msgstr "所有者" @@ -1330,17 +1330,17 @@ msgid "Project ID not matches between object and project" msgstr "專案ID不符合物件與專案" #: taiga/projects/attachments/models.py:41 -#: taiga/projects/custom_attributes/models.py:42 -#: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:50 -#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:499 -#: taiga/projects/models.py:521 taiga/projects/models.py:558 -#: taiga/projects/models.py:586 taiga/projects/models.py:612 -#: taiga/projects/models.py:642 taiga/projects/models.py:662 -#: taiga/projects/models.py:686 taiga/projects/models.py:714 +#: taiga/projects/custom_attributes/models.py:43 +#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 +#: taiga/projects/milestones/models.py:45 taiga/projects/models.py:500 +#: taiga/projects/models.py:522 taiga/projects/models.py:559 +#: taiga/projects/models.py:587 taiga/projects/models.py:613 +#: taiga/projects/models.py:643 taiga/projects/models.py:663 +#: taiga/projects/models.py:687 taiga/projects/models.py:715 #: taiga/projects/notifications/models.py:74 -#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:42 -#: taiga/projects/userstories/models.py:66 taiga/projects/wiki/models.py:33 -#: taiga/projects/wiki/models.py:71 taiga/users/models.py:303 +#: taiga/projects/notifications/models.py:91 taiga/projects/tasks/models.py:43 +#: taiga/projects/userstories/models.py:67 taiga/projects/wiki/models.py:34 +#: taiga/projects/wiki/models.py:72 taiga/users/models.py:303 msgid "project" msgstr "專案" @@ -1353,11 +1353,11 @@ msgid "object id" msgstr "物件ID" #: taiga/projects/attachments/models.py:51 -#: taiga/projects/custom_attributes/models.py:47 -#: taiga/projects/epics/models.py:50 taiga/projects/issues/models.py:55 -#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:158 -#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:50 -#: taiga/projects/userstories/models.py:89 taiga/projects/wiki/models.py:46 +#: taiga/projects/custom_attributes/models.py:48 +#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 +#: taiga/projects/milestones/models.py:52 taiga/projects/models.py:159 +#: taiga/projects/models.py:740 taiga/projects/tasks/models.py:51 +#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/userstorage/models.py:31 msgid "modified date" msgstr "修改日期" @@ -1375,13 +1375,13 @@ msgid "is deprecated" msgstr "棄用" #: taiga/projects/attachments/models.py:62 -#: taiga/projects/custom_attributes/models.py:40 -#: taiga/projects/epics/models.py:100 taiga/projects/milestones/models.py:58 -#: taiga/projects/models.py:515 taiga/projects/models.py:548 -#: taiga/projects/models.py:582 taiga/projects/models.py:606 -#: taiga/projects/models.py:638 taiga/projects/models.py:658 -#: taiga/projects/models.py:680 taiga/projects/models.py:710 -#: taiga/projects/wiki/models.py:76 taiga/users/models.py:298 +#: taiga/projects/custom_attributes/models.py:41 +#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:58 +#: taiga/projects/models.py:516 taiga/projects/models.py:549 +#: taiga/projects/models.py:583 taiga/projects/models.py:607 +#: taiga/projects/models.py:639 taiga/projects/models.py:659 +#: taiga/projects/models.py:681 taiga/projects/models.py:711 +#: taiga/projects/wiki/models.py:77 taiga/users/models.py:298 msgid "order" msgstr "次序" @@ -1433,29 +1433,29 @@ msgstr "日期" msgid "Url" msgstr "" -#: taiga/projects/custom_attributes/models.py:39 +#: taiga/projects/custom_attributes/models.py:40 #: taiga/projects/issues/models.py:45 msgid "type" msgstr "類型" -#: taiga/projects/custom_attributes/models.py:94 +#: taiga/projects/custom_attributes/models.py:95 msgid "values" msgstr "價值" -#: taiga/projects/custom_attributes/models.py:104 +#: taiga/projects/custom_attributes/models.py:105 msgid "epic" msgstr "" -#: taiga/projects/custom_attributes/models.py:120 -#: taiga/projects/tasks/models.py:34 taiga/projects/userstories/models.py:37 +#: taiga/projects/custom_attributes/models.py:121 +#: taiga/projects/tasks/models.py:35 taiga/projects/userstories/models.py:38 msgid "user story" msgstr "使用者故事" -#: taiga/projects/custom_attributes/models.py:136 +#: taiga/projects/custom_attributes/models.py:137 msgid "task" msgstr "任務 " -#: taiga/projects/custom_attributes/models.py:152 +#: taiga/projects/custom_attributes/models.py:153 msgid "issue" msgstr "問題 " @@ -1467,47 +1467,47 @@ msgstr "已存在相同姓名" msgid "You don't have permissions to set this status to this epic." msgstr "" -#: taiga/projects/epics/models.py:34 taiga/projects/issues/models.py:35 -#: taiga/projects/tasks/models.py:36 taiga/projects/userstories/models.py:61 +#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 +#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 msgid "ref" msgstr "ref" -#: taiga/projects/epics/models.py:41 taiga/projects/issues/models.py:39 -#: taiga/projects/tasks/models.py:40 taiga/projects/userstories/models.py:71 +#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 +#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 msgid "status" msgstr "狀態" -#: taiga/projects/epics/models.py:44 +#: taiga/projects/epics/models.py:45 msgid "epics order" msgstr "" -#: taiga/projects/epics/models.py:53 taiga/projects/issues/models.py:59 -#: taiga/projects/tasks/models.py:54 taiga/projects/userstories/models.py:93 +#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 +#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 msgid "subject" msgstr "主旨" -#: taiga/projects/epics/models.py:57 taiga/projects/models.py:519 -#: taiga/projects/models.py:554 taiga/projects/models.py:610 -#: taiga/projects/models.py:640 taiga/projects/models.py:660 -#: taiga/projects/models.py:684 taiga/projects/models.py:712 +#: taiga/projects/epics/models.py:58 taiga/projects/models.py:520 +#: taiga/projects/models.py:555 taiga/projects/models.py:611 +#: taiga/projects/models.py:641 taiga/projects/models.py:661 +#: taiga/projects/models.py:685 taiga/projects/models.py:713 #: taiga/users/models.py:139 msgid "color" msgstr "顏色" -#: taiga/projects/epics/models.py:60 taiga/projects/issues/models.py:63 -#: taiga/projects/tasks/models.py:64 taiga/projects/userstories/models.py:97 +#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 +#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 msgid "assigned to" msgstr "指派給" -#: taiga/projects/epics/models.py:62 taiga/projects/userstories/models.py:99 +#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 msgid "is client requirement" msgstr "客戶要求" -#: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:101 +#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 msgid "is team requirement" msgstr "團隊要求" -#: taiga/projects/epics/models.py:68 +#: taiga/projects/epics/models.py:69 msgid "user stories" msgstr "" @@ -1643,7 +1643,7 @@ msgid "To:" msgstr "給:" #: taiga/projects/history/templatetags/functions.py:26 -#: taiga/projects/wiki/models.py:37 +#: taiga/projects/wiki/models.py:38 msgid "content" msgstr "內容" @@ -1684,17 +1684,17 @@ msgstr "嚴重性" msgid "priority" msgstr "優先性" -#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:45 -#: taiga/projects/userstories/models.py:64 +#: taiga/projects/issues/models.py:48 taiga/projects/tasks/models.py:46 +#: taiga/projects/userstories/models.py:65 msgid "milestone" msgstr "里程碑" -#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:52 +#: taiga/projects/issues/models.py:57 taiga/projects/tasks/models.py:53 msgid "finished date" msgstr "完成日期" -#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:69 -#: taiga/projects/userstories/models.py:108 +#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70 +#: taiga/projects/userstories/models.py:109 msgid "external reference" msgstr "外部參考" @@ -1706,10 +1706,10 @@ msgstr "喜歡" msgid "Likes" msgstr "喜歡" -#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:146 -#: taiga/projects/models.py:513 taiga/projects/models.py:546 -#: taiga/projects/models.py:604 taiga/projects/models.py:678 -#: taiga/projects/models.py:730 taiga/projects/wiki/models.py:35 +#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:147 +#: taiga/projects/models.py:514 taiga/projects/models.py:547 +#: taiga/projects/models.py:605 taiga/projects/models.py:679 +#: taiga/projects/models.py:731 taiga/projects/wiki/models.py:36 #: taiga/users/admin.py:58 taiga/users/models.py:294 msgid "slug" msgstr "代稱" @@ -1722,9 +1722,9 @@ msgstr "预計開始日期" msgid "estimated finish date" msgstr "預計完成日期" -#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:517 -#: taiga/projects/models.py:550 taiga/projects/models.py:608 -#: taiga/projects/models.py:682 +#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:518 +#: taiga/projects/models.py:551 taiga/projects/models.py:609 +#: taiga/projects/models.py:683 msgid "is closed" msgstr "被關閉" @@ -1753,233 +1753,233 @@ msgstr "'{param}' 參數為必要" msgid "'project' parameter is mandatory" msgstr "'project'參數為必要" -#: taiga/projects/models.py:75 +#: taiga/projects/models.py:76 msgid "email" msgstr "電子郵件" -#: taiga/projects/models.py:77 +#: taiga/projects/models.py:78 msgid "create at" msgstr "創建於" -#: taiga/projects/models.py:79 taiga/users/models.py:154 +#: taiga/projects/models.py:80 taiga/users/models.py:154 msgid "token" msgstr "代號" -#: taiga/projects/models.py:85 +#: taiga/projects/models.py:86 msgid "invitation extra text" msgstr "額外文案邀請" -#: taiga/projects/models.py:88 taiga/projects/models.py:734 +#: taiga/projects/models.py:89 taiga/projects/models.py:735 msgid "user order" msgstr "使用者次序" -#: taiga/projects/models.py:104 +#: taiga/projects/models.py:105 msgid "The user is already member of the project" msgstr "使用者已是專案成員" -#: taiga/projects/models.py:111 +#: taiga/projects/models.py:112 msgid "default epic status" msgstr "" -#: taiga/projects/models.py:115 +#: taiga/projects/models.py:116 msgid "default US status" msgstr "預設使用者故事狀態" -#: taiga/projects/models.py:118 +#: taiga/projects/models.py:119 msgid "default points" msgstr "預設點數" -#: taiga/projects/models.py:122 +#: taiga/projects/models.py:123 msgid "default task status" msgstr "預設任務狀態" -#: taiga/projects/models.py:125 +#: taiga/projects/models.py:126 msgid "default priority" msgstr "預設優先性" -#: taiga/projects/models.py:128 +#: taiga/projects/models.py:129 msgid "default severity" msgstr "預設嚴重性" -#: taiga/projects/models.py:132 +#: taiga/projects/models.py:133 msgid "default issue status" msgstr "預設問題狀態" -#: taiga/projects/models.py:136 +#: taiga/projects/models.py:137 msgid "default issue type" msgstr "預設議題類型" -#: taiga/projects/models.py:152 +#: taiga/projects/models.py:153 msgid "logo" msgstr "圖標" -#: taiga/projects/models.py:162 +#: taiga/projects/models.py:163 msgid "members" msgstr "成員" -#: taiga/projects/models.py:165 +#: taiga/projects/models.py:166 msgid "total of milestones" msgstr "全部里程碑" -#: taiga/projects/models.py:166 +#: taiga/projects/models.py:167 msgid "total story points" msgstr "全部故事點數" -#: taiga/projects/models.py:169 taiga/projects/models.py:745 +#: taiga/projects/models.py:170 taiga/projects/models.py:746 msgid "active epics panel" msgstr "" -#: taiga/projects/models.py:171 taiga/projects/models.py:747 +#: taiga/projects/models.py:172 taiga/projects/models.py:748 msgid "active backlog panel" msgstr "活躍的待辦任務優先表面板" -#: taiga/projects/models.py:173 taiga/projects/models.py:749 +#: taiga/projects/models.py:174 taiga/projects/models.py:750 msgid "active kanban panel" msgstr "活躍的看板式面板" -#: taiga/projects/models.py:175 taiga/projects/models.py:751 +#: taiga/projects/models.py:176 taiga/projects/models.py:752 msgid "active wiki panel" msgstr "活躍的維基面板" -#: taiga/projects/models.py:177 taiga/projects/models.py:753 +#: taiga/projects/models.py:178 taiga/projects/models.py:754 msgid "active issues panel" msgstr "活躍的問題面板" -#: taiga/projects/models.py:180 taiga/projects/models.py:756 +#: taiga/projects/models.py:181 taiga/projects/models.py:757 msgid "videoconference system" msgstr "視訊會議系統" -#: taiga/projects/models.py:182 taiga/projects/models.py:758 +#: taiga/projects/models.py:183 taiga/projects/models.py:759 msgid "videoconference extra data" msgstr "視訊會議額外資料" -#: taiga/projects/models.py:188 +#: taiga/projects/models.py:189 msgid "creation template" msgstr "創建模版" -#: taiga/projects/models.py:191 taiga/users/admin.py:62 +#: taiga/projects/models.py:192 taiga/users/admin.py:62 msgid "is private" msgstr "私密" -#: taiga/projects/models.py:193 +#: taiga/projects/models.py:194 msgid "anonymous permissions" msgstr "匿名權限" -#: taiga/projects/models.py:195 +#: taiga/projects/models.py:196 msgid "user permissions" msgstr "使用者權限" -#: taiga/projects/models.py:198 +#: taiga/projects/models.py:199 msgid "is featured" msgstr " 受矚目的" -#: taiga/projects/models.py:201 +#: taiga/projects/models.py:202 msgid "is looking for people" msgstr "正在找人" -#: taiga/projects/models.py:203 +#: taiga/projects/models.py:204 msgid "loking for people note" msgstr "" -#: taiga/projects/models.py:217 +#: taiga/projects/models.py:218 msgid "project transfer token" msgstr "" -#: taiga/projects/models.py:221 +#: taiga/projects/models.py:222 msgid "blocked code" msgstr "" -#: taiga/projects/models.py:225 taiga/projects/notifications/models.py:66 +#: taiga/projects/models.py:226 taiga/projects/notifications/models.py:66 msgid "updated date time" msgstr "更新日期時間" -#: taiga/projects/models.py:228 taiga/projects/models.py:240 +#: taiga/projects/models.py:229 taiga/projects/models.py:241 #: taiga/projects/votes/models.py:30 msgid "count" msgstr "數量" -#: taiga/projects/models.py:231 +#: taiga/projects/models.py:232 msgid "fans last week" msgstr "上週粉絲" -#: taiga/projects/models.py:234 +#: taiga/projects/models.py:235 msgid "fans last month" msgstr "上個月粉絲" -#: taiga/projects/models.py:237 +#: taiga/projects/models.py:238 msgid "fans last year" msgstr "去年粉絲" -#: taiga/projects/models.py:243 +#: taiga/projects/models.py:244 msgid "activity last week" msgstr "上週活躍成員" -#: taiga/projects/models.py:246 +#: taiga/projects/models.py:247 msgid "activity last month" msgstr "上月活躍成員" -#: taiga/projects/models.py:249 +#: taiga/projects/models.py:250 msgid "activity last year" msgstr "去年活躍成員" -#: taiga/projects/models.py:500 +#: taiga/projects/models.py:501 msgid "modules config" msgstr "模組設定" -#: taiga/projects/models.py:552 +#: taiga/projects/models.py:553 msgid "is archived" msgstr "已歸檔" -#: taiga/projects/models.py:556 +#: taiga/projects/models.py:557 msgid "work in progress limit" msgstr "工作進度限制" -#: taiga/projects/models.py:584 taiga/userstorage/models.py:33 +#: taiga/projects/models.py:585 taiga/userstorage/models.py:33 msgid "value" msgstr "價值" -#: taiga/projects/models.py:742 +#: taiga/projects/models.py:743 msgid "default owner's role" msgstr "預設所有者角色" -#: taiga/projects/models.py:760 +#: taiga/projects/models.py:761 msgid "default options" msgstr "預設選項" -#: taiga/projects/models.py:761 +#: taiga/projects/models.py:762 msgid "epic statuses" msgstr "" -#: taiga/projects/models.py:762 +#: taiga/projects/models.py:763 msgid "us statuses" msgstr "我們狀況" -#: taiga/projects/models.py:763 taiga/projects/userstories/models.py:43 -#: taiga/projects/userstories/models.py:76 +#: taiga/projects/models.py:764 taiga/projects/userstories/models.py:44 +#: taiga/projects/userstories/models.py:77 msgid "points" msgstr "點數" -#: taiga/projects/models.py:764 +#: taiga/projects/models.py:765 msgid "task statuses" msgstr "任務狀況" -#: taiga/projects/models.py:765 +#: taiga/projects/models.py:766 msgid "issue statuses" msgstr "問題狀況" -#: taiga/projects/models.py:766 +#: taiga/projects/models.py:767 msgid "issue types" msgstr "問題類型" -#: taiga/projects/models.py:767 +#: taiga/projects/models.py:768 msgid "priorities" msgstr "優先性" -#: taiga/projects/models.py:768 +#: taiga/projects/models.py:769 msgid "severities" msgstr "嚴重性" -#: taiga/projects/models.py:769 +#: taiga/projects/models.py:770 msgid "roles" msgstr "角色" @@ -2949,9 +2949,9 @@ msgid "The color is not a valid HEX color." msgstr "" #: taiga/projects/tagging/validators.py:67 -#: taiga/projects/tagging/validators.py:92 -#: taiga/projects/tagging/validators.py:105 -#: taiga/projects/tagging/validators.py:112 +#: taiga/projects/tagging/validators.py:101 +#: taiga/projects/tagging/validators.py:114 +#: taiga/projects/tagging/validators.py:121 msgid "The tag doesn't exist." msgstr "" @@ -2967,15 +2967,15 @@ msgstr "無權限更動此務下的使用者故事" msgid "You don't have permissions to set this status to this task." msgstr "無權限更動此任務下的狀態" -#: taiga/projects/tasks/models.py:57 +#: taiga/projects/tasks/models.py:58 msgid "us order" msgstr "使用者故事次序" -#: taiga/projects/tasks/models.py:59 +#: taiga/projects/tasks/models.py:60 msgid "taskboard order" msgstr "任務板次序" -#: taiga/projects/tasks/models.py:67 +#: taiga/projects/tasks/models.py:68 msgid "is iocaine" msgstr "挑戰全新任務" @@ -3621,58 +3621,58 @@ msgstr "產品所有人" msgid "Stakeholder" msgstr "利害關係人" -#: taiga/projects/userstories/api.py:119 +#: taiga/projects/userstories/api.py:124 msgid "You don't have permissions to set this sprint to this user story." msgstr "無權限更動使用者故事的衝刺任務" -#: taiga/projects/userstories/api.py:123 +#: taiga/projects/userstories/api.py:128 msgid "You don't have permissions to set this status to this user story." msgstr "無權限更動此使用者故事的狀態" -#: taiga/projects/userstories/api.py:213 +#: taiga/projects/userstories/api.py:218 #, python-brace-format msgid "Invalid role id '{role_id}'" msgstr "" -#: taiga/projects/userstories/api.py:220 +#: taiga/projects/userstories/api.py:225 #, python-brace-format msgid "Invalid points id '{points_id}'" msgstr "" -#: taiga/projects/userstories/api.py:235 +#: taiga/projects/userstories/api.py:240 #, python-brace-format msgid "Generating the user story #{ref} - {subject}" msgstr "産生使用者故事 #{ref} - {subject}" -#: taiga/projects/userstories/api.py:296 +#: taiga/projects/userstories/api.py:301 msgid "ref param is needed" msgstr "" -#: taiga/projects/userstories/api.py:299 +#: taiga/projects/userstories/api.py:304 msgid "project or project_slug param is needed" msgstr "" -#: taiga/projects/userstories/models.py:40 +#: taiga/projects/userstories/models.py:41 msgid "role" msgstr "角色" -#: taiga/projects/userstories/models.py:79 +#: taiga/projects/userstories/models.py:80 msgid "backlog order" msgstr "待辦任務先後次序" -#: taiga/projects/userstories/models.py:81 +#: taiga/projects/userstories/models.py:82 msgid "sprint order" msgstr "衝刺次序" -#: taiga/projects/userstories/models.py:83 +#: taiga/projects/userstories/models.py:84 msgid "kanban order" msgstr "" -#: taiga/projects/userstories/models.py:91 +#: taiga/projects/userstories/models.py:92 msgid "finish date" msgstr "完成日期" -#: taiga/projects/userstories/models.py:106 +#: taiga/projects/userstories/models.py:107 msgid "generated from issue" msgstr "産生自問題 " @@ -3781,11 +3781,11 @@ msgstr "'content'參數為必要" msgid "'project_id' parameter is mandatory" msgstr "'project_id'參數為必要" -#: taiga/projects/wiki/models.py:41 +#: taiga/projects/wiki/models.py:42 msgid "last modifier" msgstr "上次更改" -#: taiga/projects/wiki/models.py:74 +#: taiga/projects/wiki/models.py:75 msgid "href" msgstr "href" From d05b3f197fe6b89900ca18a07fc83ad53a92e08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Sep 2016 13:11:01 +0200 Subject: [PATCH 258/261] Remove tags_color in superadmin panel to prevent some errors --- taiga/projects/admin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/taiga/projects/admin.py b/taiga/projects/admin.py index eecf7fef..9f7f5431 100644 --- a/taiga/projects/admin.py +++ b/taiga/projects/admin.py @@ -106,8 +106,7 @@ class ProjectAdmin(admin.ModelAdmin): (_("Extra info"), { "classes": ("collapse",), "fields": ("creation_template", - ("is_looking_for_people", "looking_for_people_note"), - "tags_colors"), + ("is_looking_for_people", "looking_for_people_note")), }), (_("Modules"), { "classes": ("collapse",), From 600392c3e106a8250dde97bdd374144106c39582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Sep 2016 13:56:00 +0200 Subject: [PATCH 259/261] Remove duplicate line --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7aa93c..108833e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ - ProjectTemplates now are sorted by the attribute 'order'. - Create enpty wiki pages (if not exist) when a new link is created. - Diff messages in history entries now show only the relevant changes (with some context). -- Include created, modified and finished dates for tasks in CSV reports - User stories and tasks listing API call support extra params to include more data (tasks and attachemnts and attachments, respectively) - Comments: - Now comment owners and project admins can edit existing comments with the history Entry endpoint. From fb562be12eb0fcce7475a5f6a8d42ef759852833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 28 Sep 2016 20:54:10 +0200 Subject: [PATCH 260/261] Fix userstories filter data counters --- taiga/projects/userstories/services.py | 62 ++++++++++++++------------ 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 7cb0be4f..ea342abd 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -277,7 +277,9 @@ def _get_userstories_statuses(project, queryset): SELECT DISTINCT "userstories_userstory"."status_id" "status_id", "userstories_userstory"."id" "us_id" FROM "userstories_userstory" - LEFT JOIN "epics_relateduserstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT OUTER JOIN "epics_relateduserstory" ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" WHERE {where} ), @@ -294,7 +296,7 @@ def _get_userstories_statuses(project, queryset): "projects_userstorystatus"."order", COALESCE("counters"."count", 0) FROM "projects_userstorystatus" - LEFT JOIN "counters" + LEFT OUTER JOIN "counters" ON "counters"."status_id" = "projects_userstorystatus"."id" WHERE "projects_userstorystatus"."project_id" = %s ORDER BY "projects_userstorystatus"."order"; @@ -327,7 +329,9 @@ def _get_userstories_assigned_to(project, queryset): SELECT DISTINCT "userstories_userstory"."assigned_to_id" "assigned_to_id", "userstories_userstory"."id" "us_id" FROM "userstories_userstory" - LEFT JOIN "epics_relateduserstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT OUTER JOIN "epics_relateduserstory" ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" WHERE {where} ), @@ -360,7 +364,7 @@ def _get_userstories_assigned_to(project, queryset): FROM "userstories_userstory" INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") - LEFT JOIN "epics_relateduserstory" + LEFT OUTER JOIN "epics_relateduserstory" ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL GROUP BY "assigned_to_id" @@ -404,6 +408,8 @@ def _get_userstories_owners(project, queryset): SELECT DISTINCT "userstories_userstory"."owner_id" "owner_id", "userstories_userstory"."id" "us_id" FROM "userstories_userstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") LEFT OUTER JOIN "epics_relateduserstory" ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") WHERE {where} @@ -462,31 +468,31 @@ def _get_userstories_tags(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH "userstories_tags" AS ( - SELECT "tag", - COUNT("tag") "counter" - FROM ( - SELECT DISTINCT "userstories_userstory"."id" "us_id", - UNNEST("userstories_userstory"."tags") "tag" - FROM "userstories_userstory" - INNER JOIN "projects_project" - ON ("userstories_userstory"."project_id" = "projects_project"."id") - LEFT JOIN "epics_relateduserstory" - ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") - WHERE {where} - ) "tags" - GROUP BY "tag"), + WITH "userstories_tags" AS ( + SELECT "tag", + COUNT("tag") "counter" + FROM ( + SELECT DISTINCT "userstories_userstory"."id" "us_id", + UNNEST("userstories_userstory"."tags") "tag" + FROM "userstories_userstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT OUTER JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + WHERE {where} + ) "tags" + GROUP BY "tag"), - "project_tags" AS ( - SELECT reduce_dim("tags_colors") "tag_color" - FROM "projects_project" - WHERE "id"=%s) + "project_tags" AS ( + SELECT reduce_dim("tags_colors") "tag_color" + FROM "projects_project" + WHERE "id"=%s) - SELECT "tag_color"[1] "tag", COALESCE("userstories_tags"."counter", 0) "counter" - FROM "project_tags" - LEFT JOIN "userstories_tags" - ON "project_tags"."tag_color"[1] = "userstories_tags"."tag" - ORDER BY "tag" + SELECT "tag_color"[1] "tag", COALESCE("userstories_tags"."counter", 0) "counter" + FROM "project_tags" +LEFT OUTER JOIN "userstories_tags" + ON "project_tags"."tag_color"[1] = "userstories_tags"."tag" + ORDER BY "tag" """.format(where=where) with closing(connection.cursor()) as cursor: @@ -546,7 +552,7 @@ def _get_userstories_epics(project, queryset): ON ("counters"."epic_id" = "epics_epic"."id") WHERE "epics_epic"."project_id" = %s """.format(where=where) - + with closing(connection.cursor()) as cursor: cursor.execute(extra_sql, where_params + where_params + [project.id]) rows = cursor.fetchall() From 5472c993f8dbef9fd6316fbd55a3d9bc6e27f17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 29 Sep 2016 14:24:43 +0200 Subject: [PATCH 261/261] Add colors in tags for yhr filter data endpoints --- taiga/projects/epics/services.py | 7 +++-- taiga/projects/issues/services.py | 39 +++++++++++++++----------- taiga/projects/tasks/services.py | 7 +++-- taiga/projects/userstories/services.py | 7 +++-- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index ed8f127a..2921a35e 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -394,7 +394,9 @@ def _get_epics_tags(project, queryset): FROM projects_project WHERE id=%s) - SELECT tag_color[1] tag, COALESCE(epics_tags.counter, 0) counter + SELECT tag_color[1] tag, + tag_color[2] color, + COALESCE(epics_tags.counter, 0) counter FROM project_tags LEFT JOIN epics_tags ON project_tags.tag_color[1] = epics_tags.tag ORDER BY tag @@ -405,9 +407,10 @@ def _get_epics_tags(project, queryset): rows = cursor.fetchall() result = [] - for name, count in rows: + for name, color, count in rows: result.append({ "name": name, + "color": color, "count": count, }) return sorted(result, key=itemgetter("name")) diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index 782a184c..ad87a61e 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -427,24 +427,28 @@ def _get_issues_tags(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH issues_tags AS ( - SELECT tag, - COUNT(tag) counter FROM ( - SELECT UNNEST(issues_issue.tags) tag - FROM issues_issue - INNER JOIN projects_project - ON (issues_issue.project_id = projects_project.id) - WHERE {where}) tags - GROUP BY tag), - project_tags AS ( - SELECT reduce_dim(tags_colors) tag_color - FROM projects_project - WHERE id=%s) + WITH "issues_tags" AS ( + SELECT "tag", + COUNT("tag") "counter" + FROM ( + SELECT UNNEST("issues_issue"."tags") "tag" + FROM "issues_issue" + INNER JOIN "projects_project" + ON ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} + ) "tags" + GROUP BY "tag"), + "project_tags" AS ( + SELECT reduce_dim("tags_colors") "tag_color" + FROM "projects_project" + WHERE "id"=%s) - SELECT tag_color[1] tag, COALESCE(issues_tags.counter, 0) counter + SELECT "tag_color"[1] "tag", + "tag_color"[2] "color", + COALESCE("issues_tags"."counter", 0) "counter" FROM project_tags - LEFT JOIN issues_tags ON project_tags.tag_color[1] = issues_tags.tag - ORDER BY tag + LEFT JOIN "issues_tags" ON "project_tags"."tag_color"[1] = "issues_tags"."tag" + ORDER BY "tag" """.format(where=where) with closing(connection.cursor()) as cursor: @@ -452,9 +456,10 @@ def _get_issues_tags(project, queryset): rows = cursor.fetchall() result = [] - for name, count in rows: + for name, color, count in rows: result.append({ "name": name, + "color": color, "count": count, }) return sorted(result, key=itemgetter("name")) diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 055583bd..b785d373 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -352,7 +352,9 @@ def _get_tasks_tags(project, queryset): FROM projects_project WHERE id=%s) - SELECT tag_color[1] tag, COALESCE(tasks_tags.counter, 0) counter + SELECT tag_color[1] tag, + tag_color[2] color, + COALESCE(tasks_tags.counter, 0) counter FROM project_tags LEFT JOIN tasks_tags ON project_tags.tag_color[1] = tasks_tags.tag ORDER BY tag @@ -363,9 +365,10 @@ def _get_tasks_tags(project, queryset): rows = cursor.fetchall() result = [] - for name, count in rows: + for name, color, count in rows: result.append({ "name": name, + "color": color, "count": count, }) return sorted(result, key=itemgetter("name")) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index ea342abd..2a381eb0 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -488,7 +488,9 @@ def _get_userstories_tags(project, queryset): FROM "projects_project" WHERE "id"=%s) - SELECT "tag_color"[1] "tag", COALESCE("userstories_tags"."counter", 0) "counter" + SELECT "tag_color"[1] "tag", + "tag_color"[2] "color", + COALESCE("userstories_tags"."counter", 0) "counter" FROM "project_tags" LEFT OUTER JOIN "userstories_tags" ON "project_tags"."tag_color"[1] = "userstories_tags"."tag" @@ -500,9 +502,10 @@ LEFT OUTER JOIN "userstories_tags" rows = cursor.fetchall() result = [] - for name, count in rows: + for name, color, count in rows: result.append({ "name": name, + "color": color, "count": count, }) return sorted(result, key=itemgetter("name"))