From 157c9d45e03b0a540c9c736f2265e5d4ad453843 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 5 Apr 2016 18:32:35 +0200
Subject: [PATCH 01/19] Fix issue #4061: Invalid error when try to add more
memberships
---
taiga/locale/ca/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/de/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/en/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/es/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/fi/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/fr/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/it/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/nl/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/pl/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/ru/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/sv/LC_MESSAGES/django.po | 24 ++++++------
taiga/locale/tr/LC_MESSAGES/django.po | 45 +++++++++++-----------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 24 ++++++------
taiga/projects/api.py | 37 ++++++++----------
taiga/projects/services/__init__.py | 1 +
taiga/projects/services/members.py | 34 ++++++++++++++++
17 files changed, 230 insertions(+), 199 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index ed198ff2..da3a1146 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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -185,7 +185,7 @@ msgstr ""
"està corrupte."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1176,13 +1176,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "No tens permisos per a veure açò."
@@ -2394,6 +2394,14 @@ msgstr "Severitats"
msgid "Roles"
msgstr "Rols"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
@@ -3287,14 +3295,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 2fde1a47..1e58b906 100644
--- a/taiga/locale/de/LC_MESSAGES/django.po
+++ b/taiga/locale/de/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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -215,7 +215,7 @@ msgstr ""
"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:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1366,13 +1366,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Sie haben keine Berechtigungen für diese Ansicht"
@@ -2866,6 +2866,14 @@ msgstr "Gewichtung"
msgid "Roles"
msgstr "Rollen"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Zukünftiger Sprint"
@@ -3812,14 +3820,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 6d00fcbd..8bfe1422 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-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -176,7 +176,7 @@ msgid ""
msgstr ""
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1148,13 +1148,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr ""
@@ -2360,6 +2360,14 @@ msgstr ""
msgid "Roles"
msgstr ""
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
@@ -3230,14 +3238,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index f337481a..00a75a9c 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -202,7 +202,7 @@ msgid ""
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:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1346,7 +1346,7 @@ msgstr "El usuario no existe"
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
@@ -1354,7 +1354,7 @@ msgstr ""
"El proyecto debe tener un dueño y al menos uno de los usuarios debe ser un "
"administrador activo"
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "No tienes suficientes permisos para ver esto."
@@ -2801,6 +2801,14 @@ msgstr "Gravedades"
msgid "Roles"
msgstr "Roles"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
@@ -3750,14 +3758,6 @@ msgstr "No puedes tener más proyectos privados"
msgid "You can't have more public projects"
msgstr "No puedes tener más proyectos públicos"
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index b8031f95..6f73ebe2 100644
--- a/taiga/locale/fi/LC_MESSAGES/django.po
+++ b/taiga/locale/fi/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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -187,7 +187,7 @@ msgstr ""
"vioittunut."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1305,13 +1305,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Sinulla ei ole oikeuksia nähdä tätä."
@@ -2763,6 +2763,14 @@ msgstr "Vakavuudet"
msgid "Roles"
msgstr "Roolit"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Tuleva kierros"
@@ -3688,14 +3696,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index c1a5dbd5..67c37f2f 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -211,7 +211,7 @@ msgstr ""
"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:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1318,13 +1318,13 @@ msgstr "L'utilisateur n'existe pas"
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Vous n'avez pas les permissions pour consulter cet élément"
@@ -2560,6 +2560,14 @@ msgstr "Sévérités"
msgid "Roles"
msgstr "Rôles"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futurs"
@@ -3476,14 +3484,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index 2f042498..ba84680a 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -196,7 +196,7 @@ msgstr ""
"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:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1439,13 +1439,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Non hai il permesso di vedere questo elemento."
@@ -3038,6 +3038,14 @@ msgstr "Criticità"
msgid "Roles"
msgstr "Ruoli"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuri"
@@ -3988,14 +3996,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index c6cc1c9e..b1172cc3 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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -196,7 +196,7 @@ msgstr ""
"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:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1239,13 +1239,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Je hebt geen toestamming om dat te bekijken."
@@ -2483,6 +2483,14 @@ msgstr "Ernstniveaus"
msgid "Roles"
msgstr "Rollen"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Toekomstige sprint"
@@ -3381,14 +3389,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 69cce377..01eeb2dc 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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -190,7 +190,7 @@ msgstr ""
"jest uszkodzony."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1347,13 +1347,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Nie masz uprawnień by to zobaczyć."
@@ -2820,6 +2820,14 @@ msgstr "Ważność"
msgid "Roles"
msgstr "Role"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Przyszły sprint"
@@ -3752,14 +3760,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 7c9c5270..ca6d64ba 100644
--- a/taiga/locale/pt_BR/LC_MESSAGES/django.po
+++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po
@@ -19,7 +19,7 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -197,7 +197,7 @@ msgstr ""
"está corrompido."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1353,13 +1353,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Você não tem permissão para ver isso"
@@ -2804,6 +2804,14 @@ msgstr "Severidades"
msgid "Roles"
msgstr "Funções"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
@@ -3731,14 +3739,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index 512d232e..437a2093 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -200,7 +200,7 @@ msgstr ""
"изображение, либо не корректное изображение."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1354,13 +1354,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "У вас нет разрешения на просмотр."
@@ -2819,6 +2819,14 @@ msgstr "Степени важности"
msgid "Roles"
msgstr "Роли"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Будущий спринт"
@@ -3746,14 +3754,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 49e7041c..42b6dac3 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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -189,7 +189,7 @@ msgstr ""
"eller en skadad bild."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1194,13 +1194,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Du har inte behörighet att se det. "
@@ -2406,6 +2406,14 @@ msgstr "Allvarsgrad"
msgid "Roles"
msgstr "Roller"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Framtidig sprint"
@@ -3296,14 +3304,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index d5aa3d2a..591cb691 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/LC_MESSAGES/django.po
@@ -10,10 +10,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-01 11:09+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
+"PO-Revision-Date: 2016-04-04 13:40+0000\n"
+"Last-Translator: catborise \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
"MIME-Version: 1.0\n"
@@ -197,13 +196,13 @@ msgstr ""
"resim dosyası değil."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
msgid "Blocked element"
-msgstr ""
+msgstr "Engellenmiş nesne"
#: taiga/base/api/pagination.py:213
msgid "Page is not 'last', nor can it be converted to an int."
@@ -355,7 +354,7 @@ msgstr "Ön şart hatası"
#: taiga/base/exceptions.py:217
msgid "No room left for more projects."
-msgstr ""
+msgstr "Daha fazla proje için yer kalmadı."
#: taiga/base/filters.py:79 taiga/base/filters.py:444
msgid "Error in filter params types."
@@ -1291,23 +1290,23 @@ msgstr "Geçersiz şablon tanımı"
#: taiga/projects/api.py:356
msgid "Invalid user id"
-msgstr ""
+msgstr "Geçersiz kullanıcı id"
#: taiga/projects/api.py:362
msgid "The user doesn't exist"
-msgstr ""
+msgstr "Kullanıcı mevcut değil"
#: taiga/projects/api.py:366
msgid "The user must be already a project member"
-msgstr ""
+msgstr "Kullanıcı zaten proje üyesi durumunda"
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "Görebilmek için yetkiniz yok."
@@ -1406,7 +1405,7 @@ msgstr ""
#: taiga/projects/choices.py:34
msgid "This project is blocked because the owner left"
-msgstr ""
+msgstr "Yetkili kalmadığı için proje bloklandı"
#: taiga/projects/custom_attributes/choices.py:27
msgid "Text"
@@ -1422,7 +1421,7 @@ msgstr "Tarih"
#: 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
@@ -1828,7 +1827,7 @@ msgstr ""
#: taiga/projects/models.py:225
msgid "blocked code"
-msgstr ""
+msgstr "engellenmiş kod"
#: taiga/projects/models.py:229 taiga/projects/notifications/models.py:65
msgid "updated date time"
@@ -2579,6 +2578,14 @@ msgstr "Önem dereceleri"
msgid "Roles"
msgstr "Roller"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Gelecek sprint"
@@ -3464,14 +3471,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index 2136848c..86d2219b 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-04-01 11:09+0200\n"
+"POT-Creation-Date: 2016-04-05 19:23+0200\n"
"PO-Revision-Date: 2016-03-30 10:59+0000\n"
"Last-Translator: Alejandro Alonso Fernández \n"
@@ -183,7 +183,7 @@ msgid ""
msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:629
+#: taiga/hooks/api.py:68 taiga/projects/api.py:641
#: 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
@@ -1327,13 +1327,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:668
+#: taiga/projects/api.py:671
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
msgstr ""
-#: taiga/projects/api.py:708
+#: taiga/projects/api.py:705
msgid "You don't have permisions to see that."
msgstr "您無觀看權限"
@@ -2789,6 +2789,14 @@ msgstr "嚴重性"
msgid "Roles"
msgstr "角色"
+#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+msgid "You have reached your current limit of memberships for private projects"
+msgstr ""
+
+#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+msgid "You have reached your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "未來之衝刺"
@@ -3701,14 +3709,6 @@ msgstr ""
msgid "You can't have more public projects"
msgstr ""
-#: taiga/users/services.py:625
-msgid "You have reached your current limit of memberships for private projects"
-msgstr ""
-
-#: taiga/users/services.py:634
-msgid "You have reached your current limit of memberships for public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index d32115c4..07aaaab1 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -615,6 +615,18 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
else:
return self.serializer_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,
+ total_new_memberships
+ )
+ if not can_add_memberships:
+ raise exc.NotEnoughSlotsForProject(
+ project.is_private,
+ total_new_memberships,
+ error_type
+ )
+
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
serializer = serializers.MembersBulkSerializer(data=request.DATA)
@@ -628,18 +640,9 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
if project.blocked_code is not None:
raise exc.Blocked(_("Blocked element"))
- # TODO: this should be moved to main exception handler instead
- # of handling explicit exception catchin here.
-
if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list):
- members = len(data["bulk_memberships"])
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- project.owner,
- project,
- members
- )
- if not enough_slots:
- raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
+ total_new_memberships = len(data["bulk_memberships"])
+ self._check_if_project_can_have_more_memberships(project, total_new_memberships)
try:
members = services.create_members_in_bulk(data["bulk_memberships"],
@@ -665,18 +668,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
def pre_delete(self, obj):
if obj.user is not None and not services.can_user_leave_project(obj.user, obj.project):
- raise exc.BadRequest(_("The project must have an owner and at least one of the users must be an active admin"))
+ raise exc.BadRequest(_("The project must have an owner and at least one of the users "
+ "must be an active admin"))
def pre_save(self, obj):
if not obj.id:
- members = 1
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- self.request.user,
- obj.project,
- members
- )
- if not enough_slots:
- raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error)
+ self._check_if_project_can_have_more_memberships(obj.project, 1)
if not obj.token:
obj.token = str(uuid.uuid1())
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index 6227cc54..a4a1d465 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -40,6 +40,7 @@ from .members import get_members_from_bulk
from .members import remove_user_from_project, project_has_valid_admins, can_user_leave_project
from .members import get_max_memberships_for_project, get_total_project_memberships
from .members import check_if_project_privacity_can_be_changed
+from .members import check_if_project_can_have_more_memberships
from .modules_config import get_modules_config
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index f408d430..1174bdf3 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -1,4 +1,22 @@
+# 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.utils import db, text
+from django.utils.translation import ugettext as _
from .. import models
@@ -120,3 +138,19 @@ def check_if_project_privacity_can_be_changed(project):
return {'can_be_updated': False, 'reason': error_project_exceeded}
return {'can_be_updated': True, 'reason': None}
+
+
+def check_if_project_can_have_more_memberships(project, total_new_memberships):
+ if project.is_private:
+ total_memberships = project.memberships.count() + total_new_memberships
+ max_memberships = project.owner.max_memberships_private_projects
+ error_members_exceeded = _("You have reached your current limit of memberships for private projects")
+ else:
+ total_memberships = project.memberships.count() + total_new_memberships
+ max_memberships = project.owner.max_memberships_public_projects
+ error_members_exceeded = _("You have reached your current limit of memberships for public projects")
+
+ if max_memberships is not None and total_memberships > max_memberships:
+ return False, error_members_exceeded
+
+ return True, None
From 53b480c12765c23b6de16571e0406d4a073ea754 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Wed, 6 Apr 2016 19:16:51 +0200
Subject: [PATCH 02/19] Fix issue #4058: Remove memberships of deleted users
---
..._memberships_of_cancelled_users_acounts.py | 21 +++++++++++++++++++
taiga/users/models.py | 5 ++++-
tests/integration/test_users.py | 15 +++++++++++++
3 files changed, 40 insertions(+), 1 deletion(-)
create mode 100644 taiga/projects/migrations/0040_remove_memberships_of_cancelled_users_acounts.py
diff --git a/taiga/projects/migrations/0040_remove_memberships_of_cancelled_users_acounts.py b/taiga/projects/migrations/0040_remove_memberships_of_cancelled_users_acounts.py
new file mode 100644
index 00000000..e666b4b9
--- /dev/null
+++ b/taiga/projects/migrations/0040_remove_memberships_of_cancelled_users_acounts.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-04-06 15:46
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def remove_memberships_of_cancelled_users_acounts(apps, schema_editor):
+ Membership = apps.get_model("projects", "Membership")
+ Membership.objects.filter(user__is_active=False).delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('projects', '0039_auto_20160322_1157'),
+ ]
+
+ operations = [
+ migrations.RunPython(remove_memberships_of_cancelled_users_acounts),
+ ]
diff --git a/taiga/users/models.py b/taiga/users/models.py
index e205e583..a0a9048b 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -280,9 +280,12 @@ class User(AbstractBaseUser, PermissionsMixin):
self.save()
self.auth_data.all().delete()
- #Blocking all owned users
+ # Blocking all owned projects
self.owned_projects.update(blocked_code=BLOCKED_BY_OWNER_LEAVING)
+ # Remove all memberships
+ self.memberships.all().delete()
+
class Role(models.Model):
name = models.CharField(max_length=200, null=False, blank=False,
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 360c6054..4e9da6e5 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -165,6 +165,21 @@ def test_delete_self_user_blocking_projects(client):
assert project.blocked_code == project_choices.BLOCKED_BY_OWNER_LEAVING
+def test_delete_self_user_remove_membership_projects(client):
+ project = f.ProjectFactory.create()
+ user = f.UserFactory.create()
+ f.create_membership(project=project, user=user)
+
+ url = reverse('users-detail', kwargs={"pk": user.pk})
+
+ assert project.memberships.all().count() == 1
+
+ client.login(user)
+ response = client.delete(url)
+
+ assert project.memberships.all().count() == 0
+
+
def test_cancel_self_user_with_valid_token(client):
user = f.UserFactory.create()
url = reverse('users-cancel')
From 440b4e79369ba9befeca6817a21605c9d4fb8350 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Fri, 8 Apr 2016 11:49:36 +0200
Subject: [PATCH 03/19] Allowing export projects even if any attachment doesn't
exist in the storage
---
taiga/export_import/service.py | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py
index 11ea8b4f..14ecd22d 100644
--- a/taiga/export_import/service.py
+++ b/taiga/export_import/service.py
@@ -111,14 +111,15 @@ def render_project(project, outfile, chunk_size = 8190):
# We write the attached_files by chunks so the memory used is not increased
attachment_file = attachment.attached_file
- with default_storage.open(attachment_file.name) as f:
- while True:
- bin_data = f.read(chunk_size)
- if not bin_data:
- break
+ 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).decode('utf-8')
- outfile.write(b64_data)
+ b64_data = base64.b64encode(bin_data).decode('utf-8')
+ outfile.write(b64_data)
outfile.write('", \n "name":"{}"}}\n}}'.format(
os.path.basename(attachment_file.name)))
From e84015e48ef3708bcb9e06aa407f5a1d1a6c3dfb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 8 Apr 2016 12:16:57 +0200
Subject: [PATCH 04/19] Fix issue 4056, 4075 and a minor refactor
---
taiga/export_import/api.py | 117 +++++++++----------
taiga/export_import/dump_service.py | 19 +++-
taiga/locale/ca/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/de/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/en/LC_MESSAGES/django.po | 104 +++++++++--------
taiga/locale/es/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/fi/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/fr/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/it/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/nl/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/pl/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/ru/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/sv/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/locale/tr/LC_MESSAGES/django.po | 108 ++++++++++--------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 109 ++++++++++--------
taiga/projects/api.py | 21 ++--
taiga/projects/serializers.py | 2 -
taiga/projects/services/__init__.py | 6 +-
taiga/projects/services/members.py | 39 +------
taiga/projects/services/projects.py | 126 +++++++++++++++++++++
taiga/users/services.py | 68 +++--------
tests/integration/test_importer_api.py | 8 +-
tests/integration/test_projects.py | 4 +-
24 files changed, 1093 insertions(+), 837 deletions(-)
create mode 100644 taiga/projects/services/projects.py
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index 1ae5ca4b..5b570980 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -91,13 +91,18 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
data = request.DATA.copy()
data['owner'] = data.get('owner', request.user.email)
+ # Validate if the project can be imported
is_private = data.get('is_private', False)
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+ 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_service.has_available_slot_for_import_new_project(
self.request.user,
- Project(is_private=is_private, id=None)
+ is_private,
+ total_memberships
)
if not enough_slots:
- raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(is_private, total_memberships, error_message)
# Create Project
project_serialized = service.store_project(data)
@@ -115,14 +120,6 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Create memberships
if "memberships" in data:
- members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- self.request.user,
- Project(is_private=is_private, id=None),
- members
- )
- if not enough_slots:
- raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
service.store_memberships(project_serialized.object, data)
try:
@@ -200,53 +197,6 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
headers = self.get_success_headers(response_data)
return response.Created(response_data, headers=headers)
- @list_route(methods=["POST"])
- @method_decorator(atomic)
- def load_dump(self, request):
- throttle = throttling.ImportDumpModeRateThrottle()
-
- if not throttle.allow_request(request, self):
- self.throttled(request, throttle.wait())
-
- self.check_permissions(request, "load_dump", None)
-
- dump = request.FILES.get('dump', None)
-
- if not dump:
- raise exc.WrongArguments(_("Needed dump file"))
-
- reader = codecs.getreader("utf-8")
-
- try:
- dump = json.load(reader(dump))
- is_private = dump.get("is_private", False)
- except Exception:
- raise exc.WrongArguments(_("Invalid dump format"))
-
- slug = dump.get('slug', None)
- if slug is not None and Project.objects.filter(slug=slug).exists():
- del dump['slug']
-
- user = request.user
- dump['owner'] = user.email
-
- members = len([m for m in dump.get("memberships", []) if m.get("email", None) != dump["owner"]])
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- user,
- Project(is_private=is_private, id=None),
- members
- )
- if not enough_slots:
- raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error)
-
- if settings.CELERY_ENABLED:
- task = tasks.load_project_dump.delay(user, dump)
- return response.Accepted({"import_id": task.id})
-
- project = dump_service.dict_to_project(dump, request.user)
- response_data = ProjectSerializer(project).data
- return response.Created(response_data)
-
@detail_route(methods=['post'])
@method_decorator(atomic)
def issue(self, request, *args, **kwargs):
@@ -342,3 +292,54 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
headers = self.get_success_headers(wiki_link.data)
return response.Created(wiki_link.data, headers=headers)
+
+ @list_route(methods=["POST"])
+ @method_decorator(atomic)
+ def load_dump(self, request):
+ throttle = throttling.ImportDumpModeRateThrottle()
+
+ if not throttle.allow_request(request, self):
+ self.throttled(request, throttle.wait())
+
+ self.check_permissions(request, "load_dump", None)
+
+ dump = request.FILES.get('dump', None)
+
+ if not dump:
+ raise exc.WrongArguments(_("Needed dump file"))
+
+ reader = codecs.getreader("utf-8")
+
+ try:
+ dump = json.load(reader(dump))
+ except Exception:
+ raise exc.WrongArguments(_("Invalid dump format"))
+
+ slug = dump.get('slug', None)
+ if slug is not None and Project.objects.filter(slug=slug).exists():
+ del dump['slug']
+
+ user = request.user
+ dump['owner'] = user.email
+
+ # Validate if the project can be imported
+ is_private = dump.get("is_private", False)
+ total_memberships = len([m for m in dump.get("memberships", [])
+ if m.get("email", None) != dump["owner"]])
+ total_memberships = total_memberships + 1 # 1 is the owner
+ (enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
+ user,
+ is_private,
+ total_memberships
+ )
+ if not enough_slots:
+ raise exc.NotEnoughSlotsForProject(is_private, total_memberships, error_message)
+
+ if settings.CELERY_ENABLED:
+ task = tasks.load_project_dump.delay(user, dump)
+ return response.Accepted({"import_id": task.id})
+
+ project = dump_service.dict_to_project(dump, request.user)
+ response_data = ProjectSerializer(project).data
+ return response.Created(response_data)
+
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
index b68f3bf9..e7c8848f 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.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 .
+from django.db.transaction import atomic
+
+from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from taiga.projects.models import Membership, Project
@@ -88,17 +91,23 @@ def store_tags_colors(project, data):
return None
+@method_decorator(atomic)
def dict_to_project(data, owner=None):
if owner:
data["owner"] = owner.email
- members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]])
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
+
+ # Validate if the owner can have this project
+ 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
+ (enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
owner,
- Project(is_private=data.get("is_private", False), id=None),
- members
+ is_private,
+ total_memberships
)
if not enough_slots:
- raise TaigaImportError(not_enough_slots_error)
+ raise TaigaImportError(error_message)
project_serialized = service.store_project(data)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index da3a1146..10eb5a90 100644
--- a/taiga/locale/ca/LC_MESSAGES/django.po
+++ b/taiga/locale/ca/LC_MESSAGES/django.po
@@ -9,10 +9,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
"MIME-Version: 1.0\n"
@@ -185,7 +184,7 @@ msgstr ""
"està corrupte."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -469,71 +468,71 @@ msgstr ""
" Comentari: %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Es necessita arxiu dump."
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Format d'arxiu dump invàlid"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr ""
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr ""
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr ""
@@ -723,7 +722,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nom"
@@ -1176,13 +1175,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "No tens permisos per a veure açò."
@@ -1212,7 +1211,7 @@ msgstr "Amo"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "Projecte"
@@ -1251,7 +1250,7 @@ msgstr "està obsolet "
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "Ordre"
@@ -1531,7 +1530,7 @@ msgstr "Fans"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -2358,50 +2357,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Opcions per defecte"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Estatus d'històries d'usuari"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Punts"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Estatus de tasques"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Estatus d'incidéncies"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Tipus d'incidéncies"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Prioritats"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Severitats"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Rols"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
@@ -3271,7 +3292,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "permissos"
@@ -3287,14 +3308,6 @@ msgstr "Nom d'usuari invàlid"
msgid "Username or password does not matches user."
msgstr ""
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 1e58b906..5c261f3f 100644
--- a/taiga/locale/de/LC_MESSAGES/django.po
+++ b/taiga/locale/de/LC_MESSAGES/django.po
@@ -17,10 +17,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+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"
@@ -215,7 +214,7 @@ msgstr ""
"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:641
+#: 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
@@ -524,71 +523,71 @@ msgstr ""
"Kommentar: %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Es ist mindestens eine Rolle nötig"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Exportdatei erforderlich"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Ungültiges Exportdatei Format"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "Fehler beim Importieren der Projektdaten"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "Fehler beim Importieren der Listen von Projektattributen"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "Fehler beim Importieren der vorgegebenen Projekt Attributwerte "
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "Fehler beim Importieren der Kundenattribute"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "Fehler beim Importieren der Rollen"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "Fehler beim Importieren der Mitgliedschaften"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "Fehler beim Import der Sprints"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "Fehler beim Importieren von Wiki Seiten"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "Fehler beim Importieren von Wiki Links"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "Fehler beim Importieren der Tickets"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "Fehler beim Importieren der User-Stories"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "Fehler beim Importieren der Aufgaben"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "Fehler beim Importieren der Schlagworte"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "Fehler beim Importieren der Chroniken"
@@ -871,7 +870,7 @@ msgstr "Authentifizierung erforderlich"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "Name"
@@ -1366,13 +1365,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Sie haben keine Berechtigungen für diese Ansicht"
@@ -1402,7 +1401,7 @@ msgstr "Besitzer"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "Projekt"
@@ -1441,7 +1440,7 @@ msgstr "wurde verworfen"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "Reihenfolge"
@@ -1725,7 +1724,7 @@ msgstr "Likes"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "Slug"
@@ -2830,50 +2829,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Voreingestellte Optionen"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Status für User-Stories"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Punkte"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Ticket Status"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Ticket Arten"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Prioritäten"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Gewichtung"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Rollen"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Zukünftiger Sprint"
@@ -3796,7 +3817,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "Berechtigungen"
@@ -3812,14 +3833,6 @@ msgstr "Ungültiger Benutzername. Versuchen Sie es mit einem anderen."
msgid "Username or password does not matches user."
msgstr "Benutzername oder Passwort stimmen mit keinem Benutzer überein."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 8bfe1422..7e173433 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-05 19:23+0200\n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -176,7 +176,7 @@ msgid ""
msgstr ""
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -457,71 +457,71 @@ msgid ""
" "
msgstr ""
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr ""
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr ""
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr ""
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr ""
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr ""
@@ -711,7 +711,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr ""
@@ -1148,13 +1148,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr ""
@@ -1184,7 +1184,7 @@ msgstr ""
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr ""
@@ -1223,7 +1223,7 @@ msgstr ""
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr ""
@@ -1503,7 +1503,7 @@ msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr ""
@@ -2324,50 +2324,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr ""
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr ""
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr ""
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr ""
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr ""
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr ""
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr ""
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr ""
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr ""
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr ""
@@ -3214,7 +3236,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr ""
@@ -3230,14 +3252,6 @@ msgstr ""
msgid "Username or password does not matches user."
msgstr ""
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index 00a75a9c..f9b32379 100644
--- a/taiga/locale/es/LC_MESSAGES/django.po
+++ b/taiga/locale/es/LC_MESSAGES/django.po
@@ -16,10 +16,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+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"
@@ -202,7 +201,7 @@ msgid ""
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:641
+#: 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
@@ -511,71 +510,71 @@ msgstr ""
"\n"
"Comentario: %(comment)s"
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Necesitamos al menos un rol"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Se necesita el fichero con los datos exportados"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Formato de fichero de exportación inválido"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "error importando los datos del proyecto"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "error importando la listados de valores de attributos del proyecto"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
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:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "error importando los atributos personalizados"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "error importando los roles"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "error importando los miembros"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "error importando los sprints"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "error importando las páginas del wiki"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "error importando los enlaces del wiki"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "error importando las peticiones"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "error importando las historias de usuario"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "error importando las tareas"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "error importando las etiquetas"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "error importando los timelines"
@@ -853,7 +852,7 @@ msgstr "Se requiere autenticación"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "nombre"
@@ -1346,7 +1345,7 @@ msgstr "El usuario no existe"
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: taiga/projects/api.py:672
msgid ""
"The project must have an owner and at least one of the users must be an "
"active admin"
@@ -1354,7 +1353,7 @@ msgstr ""
"El proyecto debe tener un dueño y al menos uno de los usuarios debe ser un "
"administrador activo"
-#: taiga/projects/api.py:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "No tienes suficientes permisos para ver esto."
@@ -1384,7 +1383,7 @@ msgstr "Dueño"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "Proyecto"
@@ -1423,7 +1422,7 @@ msgstr "está desactualizado"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "orden"
@@ -1703,7 +1702,7 @@ msgstr "Likes"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -2765,50 +2764,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Opciones por defecto"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Estados de historia de usuario"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Puntos"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Estado de tareas"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Estados de peticion"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Tipos de petición"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Gravedades"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Roles"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
@@ -3734,7 +3755,7 @@ msgstr "máximo de membresías para cada proyecto privado poseído"
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:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "permisos"
@@ -3750,14 +3771,6 @@ msgstr "Nombre de usuario inválido. Prueba con otro."
msgid "Username or password does not matches user."
msgstr "Nombre de usuario o contraseña inválidos."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr "No puedes tener más proyectos privados"
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr "No puedes tener más proyectos públicos"
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 6f73ebe2..1d97923b 100644
--- a/taiga/locale/fi/LC_MESSAGES/django.po
+++ b/taiga/locale/fi/LC_MESSAGES/django.po
@@ -9,10 +9,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+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"
@@ -187,7 +186,7 @@ msgstr ""
"vioittunut."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -494,71 +493,71 @@ msgstr ""
"\n"
"Kommentti: %(comment)s"
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr ""
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Tarvitaan tiedosto"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Virheellinen tiedostomuoto"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "virhe projektidatan tuonnissa"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "virhe atribuuttilistan tuonnissa"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "virhe oletusarvojen tuonnissa"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "virhe omien arvojen tuonnissa"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "virhe roolien tuonnissa"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "virhe jäsenyyksien tuonnissa"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "virhe kierroksien tuonnissa"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "virhe wiki-sivujen tuonnissa"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "virhe viki-linkkien tuonnissa"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "virhe pyyntöjen tuonnissa"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "virhe käyttäjätarinoiden tuonnissa"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "virhe tehtävien tuonnissa"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "virhe avainsanojen sisäänlukemisessa"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "virhe aikajanojen tuonnissa"
@@ -833,7 +832,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "nimi"
@@ -1305,13 +1304,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Sinulla ei ole oikeuksia nähdä tätä."
@@ -1341,7 +1340,7 @@ msgstr "omistaja"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "projekti"
@@ -1380,7 +1379,7 @@ msgstr "on poistettu"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "order"
@@ -1660,7 +1659,7 @@ msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "hukka-aika"
@@ -2727,50 +2726,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Oletusoptiot"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Käyttäjätarinatilat"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Pisteet"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Tehtävien tilat"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Pyyntöjen tilat"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "pyyntötyypit"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Kiireellisyydet"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Vakavuudet"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Roolit"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Tuleva kierros"
@@ -3672,7 +3693,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "oikeudet"
@@ -3688,14 +3709,6 @@ msgstr "Tuntematon käyttäjänimi, yritä uudelleen."
msgid "Username or password does not matches user."
msgstr "Käyttäjätunnus tai salasana eivät ole oikein."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index 67c37f2f..9533481a 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -22,10 +22,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
"MIME-Version: 1.0\n"
@@ -211,7 +210,7 @@ msgstr ""
"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:641
+#: 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
@@ -528,72 +527,72 @@ msgstr ""
" Commentaire : %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Veuillez sélectionner au moins un rôle."
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Fichier de dump obligatoire"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Format de dump invalide"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "Erreur lors de l'importation de données"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
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:124
+#: taiga/export_import/dump_service.py:133
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:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "Erreur à l'importation des champs personnalisés"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "Erreur à l'importation des rôles"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "Erreur à l'importation des groupes d'utilisateurs"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "Erreur lors de l'importation des sprints."
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "Erreur à l'importation des pages Wiki"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "Erreur à l'importation des liens Wiki"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "erreur à l'importation des problèmes"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "erreur à l'importation des histoires utilisateur"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "Erreur lors de l'importation des tâches."
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "erreur lors de l'importation des mots-clés"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "erreur lors de l'import des timelines"
@@ -857,7 +856,7 @@ msgstr "Authentification requise"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "nom"
@@ -1318,13 +1317,13 @@ msgstr "L'utilisateur n'existe pas"
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Vous n'avez pas les permissions pour consulter cet élément"
@@ -1354,7 +1353,7 @@ msgstr "propriétaire"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "projet"
@@ -1393,7 +1392,7 @@ msgstr "est obsolète"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "ordre"
@@ -1673,7 +1672,7 @@ msgstr "Aime"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -2524,50 +2523,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Options par défaut"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Etats de la User Story"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Points"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Etats des tâches"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Statuts des problèmes"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Types de problèmes"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Priorités"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Sévérités"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Rôles"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futurs"
@@ -3460,7 +3481,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "permissions"
@@ -3476,14 +3497,6 @@ msgstr "Nom d'utilisateur invalide. Essayez avec un autre nom."
msgid "Username or password does not matches user."
msgstr "Aucun utilisateur avec ce nom ou ce mot de passe."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index ba84680a..1054bf89 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -14,10 +14,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
"MIME-Version: 1.0\n"
@@ -196,7 +195,7 @@ msgstr ""
"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:641
+#: 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
@@ -518,72 +517,72 @@ msgstr ""
"\n"
"Commento: %(comment)s"
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "C'è bisogno di almeno un ruolo"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "E' richiesto un file di dump"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Formato di dump invalido"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "Errore nell'importazione del progetto dati"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "Errore nell'importazione della lista degli attributi di progetto"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr ""
"Errore nell'importazione dei valori predefiniti degli attributi del progetto."
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "Errore nell'importazione degli attributi personalizzati"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "Errore nell'importazione i ruoli"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "Errore nell'importazione delle iscrizioni"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "errore nell'importazione degli sprints"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "Errore nell'importazione delle pagine wiki"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "Errore nell'importazione dei link di wiki"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "errore nell'importazione dei problemi"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "Errore nell'importazione delle user story"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "Errore nell'importazione dei compiti "
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "Errore nell'importazione dei tags"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "Errore nell'importazione delle timelines"
@@ -920,7 +919,7 @@ msgstr "E' richiesta l'autenticazione"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "nome"
@@ -1439,13 +1438,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Non hai il permesso di vedere questo elemento."
@@ -1475,7 +1474,7 @@ msgstr "proprietario"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "progetto"
@@ -1514,7 +1513,7 @@ msgstr "non approvato"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "ordine"
@@ -1794,7 +1793,7 @@ msgstr "Piaciuto"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "lumaca"
@@ -3002,50 +3001,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Opzioni predefinite"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Stati della storia utente"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Punti"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Stati del compito"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Stati del problema"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Tipologie del problema"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Priorità"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Criticità"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Ruoli"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuri"
@@ -3972,7 +3993,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "permessi"
@@ -3988,14 +4009,6 @@ msgstr "Nome utente non valido. Provane uno diverso."
msgid "Username or password does not matches user."
msgstr "Il nome utente o la password non corrispondono all'utente."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index b1172cc3..e2543498 100644
--- a/taiga/locale/nl/LC_MESSAGES/django.po
+++ b/taiga/locale/nl/LC_MESSAGES/django.po
@@ -9,10 +9,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
"MIME-Version: 1.0\n"
@@ -196,7 +195,7 @@ msgstr ""
"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:641
+#: 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
@@ -507,71 +506,71 @@ msgstr ""
" Commentaar: %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "We hadden minstens één rol nodig"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Dump file nodig"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Ongeldig dump formaat"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "fout bij het importeren van project data"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "fout bij importeren van project attributenlijst"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "fout bij importeren van standaard projectattributen waarden"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "fout bij importeren eigen attributen"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "fout bij importeren rollen"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "fout bij importeren lidmaatschappen"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "fout bij importeren sprints"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "fout bij importeren wiki pagina's"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "fout bij importeren wiki links"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "fout bij importeren issues"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "fout bij importeren user stories"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "fout bij importeren taken"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "fout bij importeren tags"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "fout bij importeren tijdlijnen"
@@ -783,7 +782,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "naam"
@@ -1239,13 +1238,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Je hebt geen toestamming om dat te bekijken."
@@ -1275,7 +1274,7 @@ msgstr "eigenaar"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "project"
@@ -1314,7 +1313,7 @@ msgstr "is verouderd"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "volgorde"
@@ -1596,7 +1595,7 @@ msgstr "Personen die dit leuk vinden"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -2447,50 +2446,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Standaard opties"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Status van User story"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Punten"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Statussen van taken"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Statussen van Issues"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Types van issue"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Prioriteiten"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Ernstniveaus"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Rollen"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Toekomstige sprint"
@@ -3365,7 +3386,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "toestemmingen"
@@ -3381,14 +3402,6 @@ msgstr "Ongeldige gebruikersnaam. Probeer met een andere."
msgid "Username or password does not matches user."
msgstr "Gebruikersnaam of wachtwoord stemt niet overeen met gebruiker."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index 01eeb2dc..f205e65f 100644
--- a/taiga/locale/pl/LC_MESSAGES/django.po
+++ b/taiga/locale/pl/LC_MESSAGES/django.po
@@ -10,10 +10,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
"MIME-Version: 1.0\n"
@@ -190,7 +189,7 @@ msgstr ""
"jest uszkodzony."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -508,71 +507,71 @@ msgstr ""
" Komentarz: %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Potrzeba conajmiej jednej roli"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Wymagany plik zrzutu"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Nieprawidłowy format zrzutu"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "błąd w trakcie importu danych projektu"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "błąd w trakcie importu atrybutów projektu"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "błąd w trakcie importu domyślnych atrybutów projektu"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "błąd w trakcie importu niestandardowych atrybutów"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "błąd w trakcie importu ról"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "błąd w trakcie importu członkostw"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "błąd w trakcie importu sprintów"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "błąd w trakcie importu stron Wiki"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "błąd w trakcie importu linków Wiki"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "błąd w trakcie importu zgłoszeń"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "błąd w trakcie importu historyjek użytkownika"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "błąd w trakcie importu zadań"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "błąd w trakcie importu tagów"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "błąd w trakcie importu osi czasu"
@@ -851,7 +850,7 @@ msgstr ""
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "nazwa"
@@ -1347,13 +1346,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Nie masz uprawnień by to zobaczyć."
@@ -1383,7 +1382,7 @@ msgstr "właściciel"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "projekt"
@@ -1422,7 +1421,7 @@ msgstr "jest przestarzałe"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "kolejność"
@@ -1702,7 +1701,7 @@ msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -2784,50 +2783,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Domyślne opcje"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Statusy historyjek użytkownika"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Punkty"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Statusy zadań"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Statusy zgłoszeń"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Typu zgłoszeń"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Priorytety"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Ważność"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Role"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Przyszły sprint"
@@ -3736,7 +3757,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "uprawnienia"
@@ -3752,14 +3773,6 @@ msgstr "Niepoprawna nazwa użytkownika. Spróbuj podać inną."
msgid "Username or password does not matches user."
msgstr "Nazwa użytkownika lub hasło są nieprawidłowe"
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index ca6d64ba..41b4e7b3 100644
--- a/taiga/locale/pt_BR/LC_MESSAGES/django.po
+++ b/taiga/locale/pt_BR/LC_MESSAGES/django.po
@@ -19,10 +19,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+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"
@@ -197,7 +196,7 @@ msgstr ""
"está corrompido."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -516,71 +515,71 @@ msgstr ""
" Comentário: %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Nós precisamos de pelo menos uma função"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Necessário de arquivo de restauração"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Formato de aquivo de restauração inválido"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "erro ao importar informações de projeto"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "erro importando lista de atributos do projeto"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "erro importando valores de atributos do projeto padrão"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "erro importando atributos personalizados"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "erro importando funcões"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "erro importando filiações"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "erro importando sprints"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "erro importando páginas wiki"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "erro importando wiki links"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "erro importando casos"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "erro importando user stories"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "erro importando tarefas"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "erro importando tags"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "erro importando linha do tempo"
@@ -858,7 +857,7 @@ msgstr "Autenticação necessária"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "Nome"
@@ -1353,13 +1352,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Você não tem permissão para ver isso"
@@ -1389,7 +1388,7 @@ msgstr "dono"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "projeto"
@@ -1428,7 +1427,7 @@ msgstr "está obsoleto"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "ordem"
@@ -1708,7 +1707,7 @@ msgstr "Curtidas"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slug"
@@ -2768,50 +2767,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Opções padrão"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Status de user story"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Pontos"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Status de tarefas"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Status de casos"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Tipos de casos"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Prioridades"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Severidades"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Funções"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Sprint futuro"
@@ -3715,7 +3736,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "permissões"
@@ -3731,14 +3752,6 @@ msgstr "Usuário inválido. Tente com um diferente."
msgid "Username or password does not matches user."
msgstr "Usuário ou senha não correspondem ao usuário"
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index 437a2093..ddcdfa3c 100644
--- a/taiga/locale/ru/LC_MESSAGES/django.po
+++ b/taiga/locale/ru/LC_MESSAGES/django.po
@@ -15,10 +15,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+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"
@@ -200,7 +199,7 @@ msgstr ""
"изображение, либо не корректное изображение."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -518,71 +517,71 @@ msgstr ""
" Комментарий: %(comment)s\n"
" "
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Нам была нужна хотя бы одна роль"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Необходим дамп-файл"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Неправильный формат дампа"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "ошибка при импорте данных проекта"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "ошибка при импорте списков свойств проекта"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "ошибка при импорте значений по умолчанию свойств проекта"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "ошибка при импорте пользовательских свойств"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "ошибка при импорте ролей"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "ошибка при импорте членства"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "ошибка при импорте спринтов"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "ошибка при импорте вики-страниц"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "ошибка при импорте вики-ссылок"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "ошибка при импорте запросов"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "ошибка импорта историй от пользователей"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "ошибка импорта задач"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "ошибка импорта тэгов"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "ошибка импорта хронологии проекта"
@@ -858,7 +857,7 @@ msgstr "Необходима аутентификация"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "имя"
@@ -1354,13 +1353,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "У вас нет разрешения на просмотр."
@@ -1390,7 +1389,7 @@ msgstr "владелец"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "проект"
@@ -1429,7 +1428,7 @@ msgstr "устаревшее"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "порядок"
@@ -1713,7 +1712,7 @@ msgstr "Лайки"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "ссылочное имя"
@@ -2783,50 +2782,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Параметры по умолчанию"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Статусу пользовательских историй"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Очки"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Статусы задачи"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Статусы запроса"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Типы запроса"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Приоритеты"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Степени важности"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Роли"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Будущий спринт"
@@ -3730,7 +3751,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "разрешения"
@@ -3746,14 +3767,6 @@ msgstr "Неверное имя пользователя. Попробуйте
msgid "Username or password does not matches user."
msgstr "Имя пользователя или пароль не соответствуют пользователю."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index 42b6dac3..d217159d 100644
--- a/taiga/locale/sv/LC_MESSAGES/django.po
+++ b/taiga/locale/sv/LC_MESSAGES/django.po
@@ -8,10 +8,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
"MIME-Version: 1.0\n"
@@ -189,7 +188,7 @@ msgstr ""
"eller en skadad bild."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -492,71 +491,71 @@ msgid ""
" "
msgstr ""
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "Vi behöver minst en roll"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "Behöver en hämtningsfil"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Invalid hämtningsfilformat"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "fel vid import av projektdata"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "fel vid import av en lista på projektegenskaper"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "fel vid import av standard projektegenskapsvärden"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "fel vid import av anpassade egenskaper"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "fel vid importering av roller"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "fel vid import av medlemskap"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "felaktig import av sprintar"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "vel vid import av wiki-sidor"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "fel vid import av wiki-länkar"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "fel vid import av ärenden"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "fel vid import av användarhistorier"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "fel vid import av uppgifter"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "fel vid importering av taggar"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "fel vid importering av tidslinje"
@@ -746,7 +745,7 @@ msgstr "Verifiering krävs"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "namn"
@@ -1194,13 +1193,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Du har inte behörighet att se det. "
@@ -1230,7 +1229,7 @@ msgstr "ägare"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "projekt"
@@ -1269,7 +1268,7 @@ msgstr "undviks"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "sortera"
@@ -1549,7 +1548,7 @@ msgstr "Gillar"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "slugg"
@@ -2370,50 +2369,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Standardval"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Status för användarhistorien"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Poäng"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Status för uppgifter"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Status för ärenden"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Ärendetyper"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Prioritet"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Allvarsgrad"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Roller"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Framtidig sprint"
@@ -3280,7 +3301,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "behörigheter"
@@ -3296,14 +3317,6 @@ msgstr "Felaktigt användarnamn. Försök med ett annat användarnamn."
msgid "Username or password does not matches user."
msgstr "Användarnamn eller lösenord passar inte."
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index 591cb691..fe7cf855 100644
--- a/taiga/locale/tr/LC_MESSAGES/django.po
+++ b/taiga/locale/tr/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-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-04-04 13:40+0000\n"
-"Last-Translator: catborise \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
"MIME-Version: 1.0\n"
@@ -196,7 +196,7 @@ msgstr ""
"resim dosyası değil."
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -501,71 +501,71 @@ msgstr ""
"\n"
"Yorumlar: %(comment)s"
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "En azından bir role ihtiyacımız var"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "İhtiyaç duyulan döküm dosyası"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "Geçersiz döküm biçemi"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "İçeri aktarılan proje verisinde hata"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
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:124
+#: taiga/export_import/dump_service.py:133
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:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "özel öznitelikler içeri aktarılırken hata"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "İçeri aktarılan rollerde hata"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "İçeri aktarılan üyeliklerde hata"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "İçeri aktarılan sprintlerde hata"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "İçeri aktarılan wiki sayfalarında hata"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "İçeri aktarılan wiki bağlantılarında hata"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "İçeri aktarılan taleplerde hata"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "İçeri aktarılan kullanıcı hikayelerinde hata"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "İçeri aktarılan görevlerde hata"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "İçeri aktarılan etiketlerde hata"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "zaman çizelgesi içeri aktarılırken hata"
@@ -840,7 +840,7 @@ msgstr "Kimlik doğrulama gerekli"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "isim"
@@ -1300,13 +1300,13 @@ msgstr "Kullanıcı mevcut değil"
msgid "The user must be already a project member"
msgstr "Kullanıcı zaten proje üyesi durumunda"
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "Görebilmek için yetkiniz yok."
@@ -1336,7 +1336,7 @@ msgstr "sahip"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "proje"
@@ -1375,7 +1375,7 @@ msgstr "kaldırıldı"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "sıra"
@@ -1655,7 +1655,7 @@ msgstr "Beğeniler"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "satır"
@@ -2542,50 +2542,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "Varsayılan ayarlar"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "Kullanıcı hikayelerinin durumları"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "Puanlar"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "Görevlerin durumları"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "Taleplerin durumları"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "Taleplerin tipleri"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "Öncelikler"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "Önem dereceleri"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "Roller"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "Gelecek sprint"
@@ -3447,7 +3469,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "izinler"
@@ -3463,14 +3485,6 @@ msgstr "Geçersiz kullanıcı adı. Farklı birşeyle yeniden deneyin."
msgid "Username or password does not matches user."
msgstr "Kullanıcı adı veya parola kullanıcıyla uyuşmuyor"
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index 86d2219b..0734e55d 100644
--- a/taiga/locale/zh-Hant/LC_MESSAGES/django.po
+++ b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
@@ -11,10 +11,9 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-05 19:23+0200\n"
-"PO-Revision-Date: 2016-03-30 10:59+0000\n"
-"Last-Translator: Alejandro Alonso Fernández \n"
+"POT-Creation-Date: 2016-04-08 13:23+0200\n"
+"PO-Revision-Date: 2016-04-08 11:23+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"
"MIME-Version: 1.0\n"
@@ -183,7 +182,7 @@ msgid ""
msgstr "上傳有效圖片,你所上傳的檔案非圖檔或已損壞"
#: taiga/base/api/mixins.py:255 taiga/base/exceptions.py:209
-#: taiga/hooks/api.py:68 taiga/projects/api.py:641
+#: 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
@@ -499,71 +498,71 @@ msgstr ""
"\n"
"評論: %(comment)s"
-#: taiga/export_import/api.py:114
+#: taiga/export_import/api.py:119
msgid "We needed at least one role"
msgstr "我們至少需要一個角色"
-#: taiga/export_import/api.py:216
+#: taiga/export_import/api.py:309
msgid "Needed dump file"
msgstr "需要的堆存檔案"
-#: taiga/export_import/api.py:224
+#: taiga/export_import/api.py:316
msgid "Invalid dump format"
msgstr "無效堆存格式"
-#: taiga/export_import/dump_service.py:106
+#: taiga/export_import/dump_service.py:115
msgid "error importing project data"
msgstr "滙入重要專案資料出錯"
-#: taiga/export_import/dump_service.py:119
+#: taiga/export_import/dump_service.py:128
msgid "error importing lists of project attributes"
msgstr "滙入標籤出錯"
-#: taiga/export_import/dump_service.py:124
+#: taiga/export_import/dump_service.py:133
msgid "error importing default project attributes values"
msgstr "滙入預設專案屬性數值出錯"
-#: taiga/export_import/dump_service.py:134
+#: taiga/export_import/dump_service.py:143
msgid "error importing custom attributes"
msgstr "滙入客制性屬出錯"
-#: taiga/export_import/dump_service.py:139
+#: taiga/export_import/dump_service.py:148
msgid "error importing roles"
msgstr "滙入角色出錯"
-#: taiga/export_import/dump_service.py:154
+#: taiga/export_import/dump_service.py:163
msgid "error importing memberships"
msgstr "滙入成員資格出錯"
-#: taiga/export_import/dump_service.py:159
+#: taiga/export_import/dump_service.py:168
msgid "error importing sprints"
msgstr "滙入衝刺任務出錯"
-#: taiga/export_import/dump_service.py:164
+#: taiga/export_import/dump_service.py:173
msgid "error importing wiki pages"
msgstr "滙入維基頁出錯"
-#: taiga/export_import/dump_service.py:169
+#: taiga/export_import/dump_service.py:178
msgid "error importing wiki links"
msgstr "滙入維基連結出錯"
-#: taiga/export_import/dump_service.py:174
+#: taiga/export_import/dump_service.py:183
msgid "error importing issues"
msgstr "滙入問題出錯"
-#: taiga/export_import/dump_service.py:179
+#: taiga/export_import/dump_service.py:188
msgid "error importing user stories"
msgstr "滙入使用者故事出錯"
-#: taiga/export_import/dump_service.py:184
+#: taiga/export_import/dump_service.py:193
msgid "error importing tasks"
msgstr "滙入任務出錯"
-#: taiga/export_import/dump_service.py:189
+#: taiga/export_import/dump_service.py:198
msgid "error importing tags"
msgstr "滙入標籤出錯"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:202
msgid "error importing timelines"
msgstr "滙入時間軸出錯"
@@ -838,7 +837,7 @@ msgstr "要求取得授權"
#: taiga/projects/models.py:536 taiga/projects/models.py:573
#: taiga/projects/models.py:596 taiga/projects/models.py:619
#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:289 taiga/webhooks/models.py:28
+#: taiga/users/models.py:292 taiga/webhooks/models.py:28
msgid "name"
msgstr "姓名"
@@ -1327,13 +1326,13 @@ msgstr ""
msgid "The user must be already a project member"
msgstr ""
-#: taiga/projects/api.py:671
+#: 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:705
+#: taiga/projects/api.py:706
msgid "You don't have permisions to see that."
msgstr "您無觀看權限"
@@ -1363,7 +1362,7 @@ msgstr "所有者"
#: 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:302
+#: taiga/projects/wiki/models.py:68 taiga/users/models.py:305
msgid "project"
msgstr "專案"
@@ -1402,7 +1401,7 @@ msgstr "棄用"
#: taiga/projects/models.py:513 taiga/projects/models.py:540
#: taiga/projects/models.py:575 taiga/projects/models.py:598
#: taiga/projects/models.py:623 taiga/projects/models.py:656
-#: taiga/projects/wiki/models.py:73 taiga/users/models.py:297
+#: taiga/projects/wiki/models.py:73 taiga/users/models.py:300
msgid "order"
msgstr "次序"
@@ -1682,7 +1681,7 @@ msgstr "喜歡"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
#: taiga/projects/models.py:474 taiga/projects/models.py:538
#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:291
+#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
msgid "slug"
msgstr "代稱"
@@ -2753,50 +2752,72 @@ msgstr ""
msgid "At least one user must be an active admin for this project."
msgstr ""
-#: taiga/projects/serializers.py:394
+#: taiga/projects/serializers.py:392
msgid "Default options"
msgstr "預設選項"
-#: taiga/projects/serializers.py:395
+#: taiga/projects/serializers.py:393
msgid "User story's statuses"
msgstr "使用者故事狀態"
-#: taiga/projects/serializers.py:396
+#: taiga/projects/serializers.py:394
msgid "Points"
msgstr "點數"
-#: taiga/projects/serializers.py:397
+#: taiga/projects/serializers.py:395
msgid "Task's statuses"
msgstr "任務狀態"
-#: taiga/projects/serializers.py:398
+#: taiga/projects/serializers.py:396
msgid "Issue's statuses"
msgstr "問題狀態"
-#: taiga/projects/serializers.py:399
+#: taiga/projects/serializers.py:397
msgid "Issue's types"
msgstr "問題類型"
-#: taiga/projects/serializers.py:400
+#: taiga/projects/serializers.py:398
msgid "Priorities"
msgstr "優先性"
-#: taiga/projects/serializers.py:401
+#: taiga/projects/serializers.py:399
msgid "Severities"
msgstr "嚴重性"
-#: taiga/projects/serializers.py:402
+#: taiga/projects/serializers.py:400
msgid "Roles"
msgstr "角色"
-#: taiga/projects/services/members.py:147 taiga/users/services.py:625
+#: taiga/projects/services/members.py:116
msgid "You have reached your current limit of memberships for private projects"
msgstr ""
-#: taiga/projects/services/members.py:151 taiga/users/services.py:634
+#: taiga/projects/services/members.py:120
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
+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
+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
+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
+msgid ""
+"This project reaches your current limit of memberships for public projects"
+msgstr ""
+
#: taiga/projects/services/stats.py:196
msgid "Future sprint"
msgstr "未來之衝刺"
@@ -3685,7 +3706,7 @@ msgstr ""
msgid "max number of memberships for each owned public project"
msgstr ""
-#: taiga/users/models.py:294
+#: taiga/users/models.py:297
msgid "permissions"
msgstr "許可"
@@ -3701,14 +3722,6 @@ msgstr "無效使用者名稱,請重試其它名稱 "
msgid "Username or password does not matches user."
msgstr "用戶名稱與密碼不符"
-#: taiga/users/services.py:602
-msgid "You can't have more private projects"
-msgstr ""
-
-#: taiga/users/services.py:612
-msgid "You can't have more public projects"
-msgstr ""
-
#: taiga/users/templates/emails/change_email-body-html.jinja:4
#, python-format
msgid ""
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 07aaaab1..a7c3aa61 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -378,13 +378,13 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
project = self.get_object()
self.check_permissions(request, "transfer_accept", project)
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
- request.user,
+ (can_transfer, error_message) = services.check_if_project_can_be_transfered(
project,
+ request.user,
)
- if not enough_slots:
+ if not can_transfer:
members = project.memberships.count()
- raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error)
+ raise exc.NotEnoughSlotsForProject(project.is_private, members, error_message)
reason = request.DATA.get('reason', None)
services.accept_project_transfer(project, request.user, token, reason)
@@ -422,12 +422,13 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
obj.owner = self.request.user
obj.template = self.request.QUERY_PARAMS.get('template', None)
- # Validate if the owner have enought slots to create or update the project
- # TODO: Move to the ProjectAdminSerializer
- (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(obj.owner, obj)
- if not enough_slots:
- members = max(obj.memberships.count(), 1)
- raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error)
+ if not obj.id or self.get_object().is_private != obj.is_private:
+ # Validate if the owner have enought slots to create the project
+ # or if you are changing the privacity
+ (can_create_or_update, error_message) = services.check_if_project_can_be_created_or_updated(obj)
+ if not can_create_or_update:
+ members = max(obj.memberships.count(), 1)
+ raise exc.NotEnoughSlotsForProject(obj.is_private, members, error_message)
self._set_base_permissions(obj)
super().pre_save(obj)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 829eec89..95f05703 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -373,8 +373,6 @@ class ProjectDetailAdminSerializer(ProjectDetailSerializer):
return services.get_max_memberships_for_project(obj)
-
-
######################################################
## Liked
######################################################
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index a4a1d465..d813423d 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -39,17 +39,19 @@ from .members import create_members_in_bulk
from .members import get_members_from_bulk
from .members import remove_user_from_project, project_has_valid_admins, can_user_leave_project
from .members import get_max_memberships_for_project, get_total_project_memberships
-from .members import check_if_project_privacity_can_be_changed
from .members import check_if_project_can_have_more_memberships
from .modules_config import get_modules_config
+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 .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 .modules_config import get_modules_config
from .transfer import request_project_transfer, start_project_transfer
from .transfer import accept_project_transfer, reject_project_transfer
diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py
index 1174bdf3..039ffd32 100644
--- a/taiga/projects/services/members.py
+++ b/taiga/projects/services/members.py
@@ -102,45 +102,14 @@ def get_total_project_memberships(project):
return project.memberships.count()
-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'
-
-def check_if_project_privacity_can_be_changed(project):
- """Return if the project privacity can be changed from private to public or viceversa.
+def check_if_project_can_have_more_memberships(project, total_new_memberships):
+ """Return if a project can have more n new memberships.
:param project: A project object.
+ :param total_new_memberships: the total of new memberships to add (int).
- :return: True if it can be changed or False if can't.
+ :return: {bool, error_mesage} return a tuple (can add new members?, error message).
"""
- if project.is_private:
- current_memberships = project.memberships.count()
- max_memberships = project.owner.max_memberships_public_projects
- error_members_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
-
- current_projects = project.owner.owned_projects.filter(is_private=False).count()
- 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_members_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
-
- current_projects = project.owner.owned_projects.filter(is_private=True).count()
- max_projects = project.owner.max_private_projects
- error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
-
- if max_memberships is not None and current_memberships > max_memberships:
- return {'can_be_updated': False, 'reason': error_members_exceeded}
-
- if max_projects is not None and current_projects >= max_projects:
- return {'can_be_updated': False, 'reason': error_project_exceeded}
-
- return {'can_be_updated': True, 'reason': None}
-
-
-def check_if_project_can_have_more_memberships(project, total_new_memberships):
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
new file mode 100644
index 00000000..306c585f
--- /dev/null
+++ b/taiga/projects/services/projects.py
@@ -0,0 +1,126 @@
+# 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 _
+
+
+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'
+
+def check_if_project_privacity_can_be_changed(project):
+ """Return if the project privacity can be changed from private to public or viceversa.
+
+ :param project: A project object.
+
+ :return: A dict like this {'can_be_updated': bool, 'reason': error message}.
+ """
+ if project.is_private:
+ current_memberships = project.memberships.count()
+ 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()
+ 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()
+ max_projects = project.owner.max_private_projects
+ error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
+
+ if max_memberships is not None and current_memberships > max_memberships:
+ return {'can_be_updated': False, 'reason': error_memberships_exceeded}
+
+ if max_projects is not None and current_projects >= max_projects:
+ return {'can_be_updated': False, 'reason': error_project_exceeded}
+
+ return {'can_be_updated': True, 'reason': None}
+
+
+def check_if_project_can_be_created_or_updated(project):
+ """Return if the project can be create or update (the privacity).
+
+ :param project: A project object.
+
+ :return: {bool, error_mesage} return a tuple (can be created or updated, error message).
+ """
+ if project.is_private:
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ max_projects = project.owner.max_private_projects
+ error_project_exceeded = _("You can't have more private projects")
+
+ current_memberships = project.memberships.count() or 1
+ max_memberships = project.owner.max_memberships_private_projects
+ error_memberships_exceeded = _("This project reaches your current limit of memberships for private projects")
+ else:
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ max_projects = project.owner.max_public_projects
+ error_project_exceeded = _("You can't have more public projects")
+
+ current_memberships = project.memberships.count() or 1
+ max_memberships = project.owner.max_memberships_public_projects
+ error_memberships_exceeded = _("This project reaches your current limit of memberships for public projects")
+
+ if max_projects is not None and current_projects >= max_projects:
+ return (False, error_project_exceeded)
+
+ if max_memberships is not None and current_memberships > max_memberships:
+ return (False, error_memberships_exceeded)
+
+ return (True, None)
+
+
+def check_if_project_can_be_transfered(project, new_owner):
+ """Return if the project can be transfered to another member.
+
+ :param project: A project object.
+ :param new_owner: The new owner.
+
+ :return: {bool, error_mesage} return a tuple (can be transfered?, error message).
+ """
+ if project.owner == new_owner:
+ return (True, None)
+
+ if project.is_private:
+ current_projects = new_owner.owned_projects.filter(is_private=True).count()
+ max_projects = new_owner.max_private_projects
+ error_project_exceeded = _("You can't have more private projects")
+
+ current_memberships = project.memberships.count()
+ max_memberships = new_owner.max_memberships_private_projects
+ error_memberships_exceeded = _("This project reaches your current limit of memberships for private projects")
+ else:
+ current_projects = new_owner.owned_projects.filter(is_private=False).count()
+ max_projects = new_owner.max_public_projects
+ error_project_exceeded = _("You can't have more public projects")
+
+ current_memberships = project.memberships.count()
+ max_memberships = new_owner.max_memberships_public_projects
+ error_memberships_exceeded = _("This project reaches your current limit of memberships for public projects")
+
+ if max_projects is not None and current_projects >= max_projects:
+ return (False, error_project_exceeded)
+
+ if max_memberships is not None and current_memberships > max_memberships:
+ return (False, error_memberships_exceeded)
+
+ return (True, None)
diff --git a/taiga/users/services.py b/taiga/users/services.py
index b18e8b36..7e210275 100644
--- a/taiga/users/services.py
+++ b/taiga/users/services.py
@@ -575,60 +575,26 @@ def get_voted_list(for_user, from_user, type=None, q=None):
]
-def has_available_slot_for_project(user, project, new_members=0):
- # TODO: Refactor: Create one service for every type of action and move to project services
- #
- # - has_available_slot_to_create_new_project()
- # - has_available_slot_to_update_this_project()
- # - has_available_slot_to_transfer_this_project()
- # - has_available_slot_to_import_this_project()
- # - has_available_slot_to_add_members_to_this_project()
-
- (enough, error) = _has_available_slot_for_project_type(user, project)
- if not enough:
- return (enough, error)
- return _has_available_slot_for_project_members(user, project, new_members)
-
-
-def _has_available_slot_for_project_type(user, project):
- if project.is_private:
- if user.max_private_projects is None:
- return (True, None)
-
- current_private_projects = user.owned_projects.filter(is_private=True).exclude(id=project.id).count()
- if current_private_projects < user.max_private_projects:
- return (True, None)
-
- return (False, _("You can't have more private projects"))
+def has_available_slot_for_import_new_project(owner, is_private, total_memberships):
+ if is_private:
+ current_projects = owner.owned_projects.filter(is_private=True).count()
+ max_projects = owner.max_private_projects
+ error_project_exceeded = _("You can't have more private projects")
+ max_memberships = owner.max_memberships_private_projects
+ error_memberships_exceeded = _("This project reaches your current limit of memberships for private projects")
else:
- if user.max_public_projects is None:
- return (True, None)
+ current_projects = owner.owned_projects.filter(is_private=False).count()
+ max_projects = owner.max_public_projects
+ error_project_exceeded = _("You can't have more public projects")
- current_public_project = user.owned_projects.filter(is_private=False).exclude(id=project.id).count()
- if current_public_project < user.max_public_projects:
- return (True, None)
+ max_memberships = owner.max_memberships_public_projects
+ error_memberships_exceeded = _("This project reaches your current limit of memberships for public projects")
- return (False, _("You can't have more public projects"))
+ if max_projects is not None and current_projects >= max_projects:
+ return (False, error_project_exceeded)
+ if max_memberships is not None and total_memberships > max_memberships:
+ return (False, error_memberships_exceeded)
-def _has_available_slot_for_project_members(user, project, new_members):
- current_memberships = max(project.memberships.count(), 1)
-
- if project.is_private:
- if user.max_memberships_private_projects is None:
- return (True, None)
-
- if current_memberships + new_members <= user.max_memberships_private_projects:
- return (True, None)
-
- return (False, _("You have reached your current limit of memberships for private projects"))
-
- else:
- if user.max_memberships_public_projects is None:
- return (True, None)
-
- if current_memberships + new_members <= user.max_memberships_public_projects:
- return (True, None)
-
- return (False, _("You have reached your current limit of memberships for public projects"))
+ return (True, None)
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index 5b6a178c..a881e67d 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -1061,7 +1061,7 @@ def test_dict_to_project_with_no_members_private_project_slots_available(client)
with pytest.raises(TaigaImportError) as excinfo:
project = dict_to_project(data, owner=user)
- assert "reached your current limit of memberships for private" in str(excinfo.value)
+ assert "reaches your current limit of memberships for private" in str(excinfo.value)
def test_dict_to_project_with_no_members_public_project_slots_available(client):
@@ -1096,7 +1096,7 @@ def test_dict_to_project_with_no_members_public_project_slots_available(client):
with pytest.raises(TaigaImportError) as excinfo:
project = dict_to_project(data, owner=user)
- assert "reached your current limit of memberships for public" in str(excinfo.value)
+ assert "reaches your current limit of memberships for public" in str(excinfo.value)
def test_invalid_dump_import(client):
@@ -1322,7 +1322,7 @@ def test_valid_dump_import_without_enough_membership_private_project_slots_one_p
response = client.post(url, {'dump': data})
assert response.status_code == 400
- assert "reached your current limit of memberships for private" in response.data["_error_message"]
+ assert "reaches your current limit of memberships for private" in response.data["_error_message"]
assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0
@@ -1369,7 +1369,7 @@ def test_valid_dump_import_without_enough_membership_public_project_slots_one_pr
response = client.post(url, {'dump': data})
assert response.status_code == 400
- assert "reached your current limit of memberships for public" in response.data["_error_message"]
+ assert "reaches your current limit of memberships for public" in response.data["_error_message"]
assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index b086f402..14a8c7f4 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1423,10 +1423,10 @@ def test_project_transfer_validate_token_from_no_admin_member_with_valid_token(c
####################################################################################
-# Test taiga.projects.services.members.check_if_project_privacity_can_be_changed
+# Test taiga.projects.services.projects.check_if_project_privacity_can_be_changed
####################################################################################
-from taiga.projects.services.members import (
+from taiga.projects.services.projects import (
check_if_project_privacity_can_be_changed,
ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS,
ERROR_MAX_PUBLIC_PROJECTS,
From 8664d781bbbed5753979ff299174efc1906eaaf0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 5 Apr 2016 17:21:18 +0200
Subject: [PATCH 05/19] Clean user role permissions: remove vote_issues in
Role.permissions
---
...e_vote_issues_in_roles_permissions_field.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
create mode 100644 taiga/users/migrations/0018_remove_vote_issues_in_roles_permissions_field.py
diff --git a/taiga/users/migrations/0018_remove_vote_issues_in_roles_permissions_field.py b/taiga/users/migrations/0018_remove_vote_issues_in_roles_permissions_field.py
new file mode 100644
index 00000000..cd9f0ae6
--- /dev/null
+++ b/taiga/users/migrations/0018_remove_vote_issues_in_roles_permissions_field.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-04-04 09:32
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0017_auto_20160208_1751'),
+ ]
+
+ operations = [
+ migrations.RunSQL(
+ "UPDATE users_role SET permissions = ARRAY_REMOVE(permissions, 'vote_issues')"
+ ),
+ ]
From 675d013474dc03eeafa83b91b49c40f97ed7ac2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Sat, 9 Apr 2016 21:42:52 +0200
Subject: [PATCH 06/19] Update CHANGELOG
---
CHANGELOG.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f955a7d8..5cc6c0a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,15 @@
# Changelog #
+## 2.1.0 ??? (unreleased)
+
+### Features
+- ...
+
+### Misc
+- Lots of small and not so small bugfixes.
+
+
## 2.0.0 Pulsatilla Patens (2016-04-04)
### Features
From e1d5bebd587d3ca7c9ce125f5f0045c016b6a0b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 14 Apr 2016 18:51:31 +0200
Subject: [PATCH 07/19] Fix and error when try to catch exceptions when fail
the import process
---
taiga/export_import/tasks.py | 39 ++++++++++++++++++++----------------
1 file changed, 22 insertions(+), 17 deletions(-)
diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py
index 8044f35c..b5a4d8fe 100644
--- a/taiga/export_import/tasks.py
+++ b/taiga/export_import/tasks.py
@@ -49,6 +49,7 @@ def dump_project(self, user, project):
render_project(project, outfile)
except Exception:
+ # Error
ctx = {
"user": user,
"error_subject": _("Error generating project dump"),
@@ -58,17 +59,17 @@ def dump_project(self, user, project):
email = mail_builder.export_error(user, ctx)
email.send()
logger.error('Error generating dump %s (by %s)', project.slug, user, exc_info=sys.exc_info())
- return
-
- deletion_date = timezone.now() + datetime.timedelta(seconds=settings.EXPORTS_TTL)
- ctx = {
- "url": url,
- "project": project,
- "user": user,
- "deletion_date": deletion_date
- }
- email = mail_builder.dump_project(user, ctx)
- email.send()
+ else:
+ # Success
+ deletion_date = timezone.now() + datetime.timedelta(seconds=settings.EXPORTS_TTL)
+ ctx = {
+ "url": url,
+ "project": project,
+ "user": user,
+ "deletion_date": deletion_date
+ }
+ email = mail_builder.dump_project(user, ctx)
+ email.send()
@app.task
@@ -81,6 +82,7 @@ def load_project_dump(user, dump):
try:
project = dict_to_project(dump, user)
except Exception:
+ # Error
ctx = {
"user": user,
"error_subject": _("Error loading project dump"),
@@ -88,9 +90,12 @@ def load_project_dump(user, dump):
}
email = mail_builder.import_error(user, ctx)
email.send()
- logger.error('Error loading dump %s (by %s)', project.slug, user, exc_info=sys.exc_info())
- return
-
- ctx = {"user": user, "project": project}
- email = mail_builder.load_dump(user, ctx)
- email.send()
+ logger.error('Error loading dump %s (by %s)',
+ dump.get("slug", "-unknow-") if dump else "-unknow-",
+ user,
+ exc_info=sys.exc_info())
+ else:
+ # Success
+ ctx = {"user": user, "project": project}
+ email = mail_builder.load_dump(user, ctx)
+ email.send()
From dbb59f74cc5da0f1a60088d12ba8b63576528d86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 15 Apr 2016 10:37:00 +0200
Subject: [PATCH 08/19] Fix import exception.
---
taiga/export_import/dump_service.py | 3 ---
taiga/export_import/tasks.py | 7 +++++--
taiga/projects/models.py | 16 +++++++++++-----
3 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
index e7c8848f..243b9167 100644
--- a/taiga/export_import/dump_service.py
+++ b/taiga/export_import/dump_service.py
@@ -15,8 +15,6 @@
# 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.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
@@ -91,7 +89,6 @@ def store_tags_colors(project, data):
return None
-@method_decorator(atomic)
def dict_to_project(data, owner=None):
if owner:
data["owner"] = owner.email
diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py
index b5a4d8fe..79880ba4 100644
--- a/taiga/export_import/tasks.py
+++ b/taiga/export_import/tasks.py
@@ -90,10 +90,13 @@ def load_project_dump(user, dump):
}
email = mail_builder.import_error(user, ctx)
email.send()
- logger.error('Error loading dump %s (by %s)',
- dump.get("slug", "-unknow-") if dump else "-unknow-",
+ logger.error('Error loading dump by %s <%s>',
user,
+ user.email,
exc_info=sys.exc_info())
+
+ # TODO: [Rollback] Remove project because it can be corrupted
+
else:
# Success
ctx = {"user": user, "project": project}
diff --git a/taiga/projects/models.py b/taiga/projects/models.py
index 59d16f9a..730a62e3 100644
--- a/taiga/projects/models.py
+++ b/taiga/projects/models.py
@@ -430,11 +430,16 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
set_notify_policy_level_to_ignore(notify_policy)
def delete_related_content(self):
- 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
+ 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()
@@ -455,6 +460,7 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
connect_all_userstories_signals()
connect_memberships_signals()
+
class ProjectModulesConfig(models.Model):
project = models.OneToOneField("Project", null=False, blank=False,
related_name="modules_config", verbose_name=_("project"))
From 45d92669e3f4022a1fe27a54800ac1843163e029 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 18 Apr 2016 15:14:22 +0200
Subject: [PATCH 09/19] Improve admin panel to list projects in user detail
panel
---
taiga/users/admin.py | 84 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 77 insertions(+), 7 deletions(-)
diff --git a/taiga/users/admin.py b/taiga/users/admin.py
index 9d4c9815..d236fe9f 100644
--- a/taiga/users/admin.py
+++ b/taiga/users/admin.py
@@ -15,12 +15,14 @@
# 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
from django.contrib import admin
from django.contrib.auth.models import Group, Permission
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
+from django.utils.translation import ugettext_lazy as _
+
from .models import Role, User
from .forms import UserChangeForm, UserCreationForm
@@ -28,6 +30,75 @@ from .forms import UserChangeForm, UserCreationForm
admin.site.unregister(Group)
+## Inlines
+
+class MembershipsInline(admin.TabularInline):
+ model = apps.get_model("projects", "Membership")
+ fk_name = "user"
+ verbose_name = _("Project Member")
+ verbose_name_plural = _("Project Members")
+ fields = ("project_id", "project_name", "project_slug", "project_is_private",
+ "project_owner", "is_admin")
+ readonly_fields = ("project_id", "project_name", "project_slug", "project_is_private",
+ "project_owner", "is_admin")
+ show_change_link = True
+ extra = 0
+
+ def project_id(self, obj):
+ return obj.project.id if obj.project else None
+ project_id.short_description = _("id")
+
+ def project_name(self, obj):
+ return obj.project.name if obj.project else None
+ project_name.short_description = _("name")
+
+ def project_slug(self, obj):
+ return obj.project.slug if obj.project else None
+ project_slug.short_description = _("slug")
+
+ def project_is_private(self, obj):
+ return obj.project.is_private if obj.project else None
+ project_is_private.short_description = _("is private")
+ project_is_private.boolean = True
+
+ 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")
+
+ def has_add_permission(self, *args):
+ return False
+
+ def has_delete_permission(self, *args):
+ return False
+
+
+class OwnedProjectsInline(admin.TabularInline):
+ model = apps.get_model("projects", "Project")
+ fk_name = "owner"
+ verbose_name = _("Project Ownership")
+ verbose_name_plural = _("Project Ownerships")
+ fields = ("id", "name", "slug", "is_private")
+ readonly_fields = ("id", "name", "slug", "is_private")
+ show_change_link = True
+ extra = 0
+
+ def has_add_permission(self, *args):
+ return False
+
+ def has_delete_permission(self, *args):
+ return False
+
+
+class RoleInline(admin.TabularInline):
+ model = Role
+ extra = 0
+
+
+## Admin panels
+
class RoleAdmin(admin.ModelAdmin):
list_display = ["name"]
filter_horizontal = ("permissions",)
@@ -42,9 +113,6 @@ class RoleAdmin(admin.ModelAdmin):
db_field, request=request, **kwargs)
-# admin.site.register(Role, RoleAdmin)
-
-
class UserAdmin(DjangoUserAdmin):
fieldsets = (
(None, {"fields": ("username", "password")}),
@@ -63,10 +131,12 @@ class UserAdmin(DjangoUserAdmin):
search_fields = ("username", "full_name", "email")
ordering = ("username",)
filter_horizontal = ()
+ inlines = [
+ OwnedProjectsInline,
+ MembershipsInline
+ ]
+
-class RoleInline(admin.TabularInline):
- model = Role
- extra = 0
admin.site.register(User, UserAdmin)
From b2ad16e5919f772230bafb394b8b4a2425912f45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Tue, 19 Apr 2016 16:00:50 +0200
Subject: [PATCH 10/19] [i18n] Update locales
---
taiga/locale/ca/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/de/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/en/LC_MESSAGES/django.po | 158 ++++++++-------
taiga/locale/es/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/fi/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/fr/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/it/LC_MESSAGES/django.po | 213 ++++++++++++---------
taiga/locale/nl/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/pl/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/pt_BR/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/ru/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/sv/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/tr/LC_MESSAGES/django.po | 160 +++++++++-------
taiga/locale/zh-Hant/LC_MESSAGES/django.po | 160 +++++++++-------
14 files changed, 1306 insertions(+), 985 deletions(-)
diff --git a/taiga/locale/ca/LC_MESSAGES/django.po b/taiga/locale/ca/LC_MESSAGES/django.po
index 10eb5a90..9deffb7e 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Catalan (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ca/)\n"
@@ -480,59 +480,59 @@ msgstr "Es necessita arxiu dump."
msgid "Invalid dump format"
msgstr "Format d'arxiu dump invàlid"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/dump_service.py:128
+#: taiga/export_import/dump_service.py:125
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:133
+#: taiga/export_import/dump_service.py:130
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/dump_service.py:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/dump_service.py:173
+#: taiga/export_import/dump_service.py:170
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/dump_service.py:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/dump_service.py:188
+#: taiga/export_import/dump_service.py:185
msgid "error importing user stories"
msgstr ""
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr ""
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr ""
@@ -555,11 +555,11 @@ msgstr "Conté camps personalitzats invàlids."
msgid "Name duplicated for the project"
msgstr ""
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr ""
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr ""
@@ -718,11 +718,12 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "Nom"
@@ -738,7 +739,7 @@ msgstr ""
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "Descripció"
@@ -776,7 +777,7 @@ msgstr "Comentari"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -810,7 +811,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informació extra"
@@ -1197,17 +1198,17 @@ msgstr ""
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1226,7 +1227,7 @@ msgstr "Id d'objecte"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1246,10 +1247,10 @@ msgstr "està obsolet "
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "Ordre"
@@ -1528,9 +1529,10 @@ msgid "Likes"
msgstr "Fans"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slug"
@@ -1542,8 +1544,8 @@ msgstr "Data estimada d'inici"
msgid "estimated finish date"
msgstr "Data estimada de finalització"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "està tancat"
@@ -1640,27 +1642,27 @@ msgstr "total de fites"
msgid "total story points"
msgstr "total de punts d'història"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "activa panell de backlog"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "activa panell de kanban"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "activa panell de wiki"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "activa panell d'incidències"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "sistema de videoconferència"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr ""
@@ -1676,7 +1678,7 @@ msgstr "permisos d'anònims"
msgid "user permissions"
msgstr "permisos d'usuaris"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "es privat"
@@ -1737,67 +1739,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "configuració de mòdules"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "està arxivat"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "limit de treball en progrés"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "rol d'amo per defecte"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "opcions per defecte"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "status d'històries d'usuari"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "punts"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "status de tasques"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "status d'incidències"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "tipus d'incidències"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "prioritats"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "severitats"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "rols"
@@ -3145,19 +3147,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Informació personal"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Permissos"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Dates importants"
diff --git a/taiga/locale/de/LC_MESSAGES/django.po b/taiga/locale/de/LC_MESSAGES/django.po
index 5c261f3f..d38d46fd 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: German (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/de/)\n"
@@ -535,59 +535,59 @@ msgstr "Exportdatei erforderlich"
msgid "Invalid dump format"
msgstr "Ungültiges Exportdatei Format"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr "Fehler beim Importieren der Projektdaten"
-#: taiga/export_import/dump_service.py:128
+#: 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:133
+#: 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:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "Fehler beim Importieren der Kundenattribute"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "Fehler beim Importieren der Rollen"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "Fehler beim Importieren der Mitgliedschaften"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "Fehler beim Import der Sprints"
-#: taiga/export_import/dump_service.py:173
+#: 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:178
+#: 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:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "Fehler beim Importieren der Tickets"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "Fehler beim Importieren der Aufgaben"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "Fehler beim Importieren der Schlagworte"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "Fehler beim Importieren der Chroniken"
@@ -610,11 +610,11 @@ 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:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Fehler beim Erzeugen der Projekt Export-Datei "
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Fehler beim Laden von Projekt Export-Datei"
@@ -866,11 +866,12 @@ msgstr "Authentifizierung erforderlich"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "Name"
@@ -886,7 +887,7 @@ msgstr "Web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "Beschreibung"
@@ -924,7 +925,7 @@ msgstr "Kommentar"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -957,7 +958,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Zusätzliche Information"
@@ -1387,17 +1388,17 @@ msgstr "Nr. unterschreidet sich zwischen dem Objekt und dem Projekt"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1416,7 +1417,7 @@ msgstr "Objekt Nr."
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1436,10 +1437,10 @@ msgstr "wurde verworfen"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "Reihenfolge"
@@ -1722,9 +1723,10 @@ msgid "Likes"
msgstr "Likes"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "Slug"
@@ -1736,8 +1738,8 @@ msgstr "geschätzter Starttermin"
msgid "estimated finish date"
msgstr "geschätzter Endtermin"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "ist geschlossen"
@@ -1834,27 +1836,27 @@ msgstr "Meilensteine Gesamt"
msgid "total story points"
msgstr "Story Punkte insgesamt"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "aktives Backlog Panel"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "aktives Kanban Panel"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "aktives Wiki Panel"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "aktives Tickets Panel"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "Videokonferenzsystem"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "Zusatzdaten Videokonferenz"
@@ -1870,7 +1872,7 @@ msgstr "Rechte für anonyme Nutzer"
msgid "user permissions"
msgstr "Rechte für registrierte Nutzer"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "ist privat"
@@ -1931,67 +1933,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "Module konfigurieren"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "ist archiviert"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "Ausführungslimit"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "Wert"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "voreingestellte Besitzerrolle"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "Vorgabe Optionen"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "User-Story Status "
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "Punkte"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "Aufgaben Status"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "Ticket Status"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "Ticket Arten"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "Prioritäten"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "Gewichtung"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "Rollen"
@@ -3668,19 +3670,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr "Prüfe die API der Historie auf Übereinstimmung"
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Personal Information"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Berechtigungen"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Wichtige Termine"
diff --git a/taiga/locale/en/LC_MESSAGES/django.po b/taiga/locale/en/LC_MESSAGES/django.po
index 7e173433..866f8483 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-08 13:23+0200\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Taiga Dev Team \n"
@@ -469,59 +469,59 @@ msgstr ""
msgid "Invalid dump format"
msgstr ""
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr ""
-#: taiga/export_import/dump_service.py:128
+#: taiga/export_import/dump_service.py:125
msgid "error importing lists of project attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:133
+#: taiga/export_import/dump_service.py:130
msgid "error importing default project attributes values"
msgstr ""
-#: taiga/export_import/dump_service.py:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr ""
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr ""
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr ""
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr ""
-#: taiga/export_import/dump_service.py:173
+#: taiga/export_import/dump_service.py:170
msgid "error importing wiki pages"
msgstr ""
-#: taiga/export_import/dump_service.py:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr ""
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr ""
-#: taiga/export_import/dump_service.py:188
+#: taiga/export_import/dump_service.py:185
msgid "error importing user stories"
msgstr ""
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr ""
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr ""
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr ""
@@ -544,11 +544,11 @@ msgstr ""
msgid "Name duplicated for the project"
msgstr ""
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr ""
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr ""
@@ -707,11 +707,12 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr ""
@@ -727,7 +728,7 @@ msgstr ""
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr ""
@@ -765,7 +766,7 @@ msgstr ""
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -791,7 +792,7 @@ msgid ""
msgstr ""
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr ""
@@ -1170,17 +1171,17 @@ msgstr ""
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1199,7 +1200,7 @@ msgstr ""
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1219,10 +1220,10 @@ msgstr ""
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr ""
@@ -1501,9 +1502,10 @@ msgid "Likes"
msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr ""
@@ -1515,8 +1517,8 @@ msgstr ""
msgid "estimated finish date"
msgstr ""
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr ""
@@ -1613,27 +1615,27 @@ msgstr ""
msgid "total story points"
msgstr ""
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr ""
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr ""
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr ""
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr ""
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr ""
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr ""
@@ -1649,7 +1651,7 @@ msgstr ""
msgid "user permissions"
msgstr ""
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr ""
@@ -1710,67 +1712,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr ""
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr ""
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr ""
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr ""
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr ""
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr ""
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr ""
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr ""
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr ""
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr ""
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr ""
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr ""
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr ""
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr ""
@@ -3094,19 +3096,39 @@ msgstr ""
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr ""
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr ""
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr ""
diff --git a/taiga/locale/es/LC_MESSAGES/django.po b/taiga/locale/es/LC_MESSAGES/django.po
index f9b32379..7538d688 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Spanish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/es/)\n"
@@ -522,59 +522,59 @@ 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:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "error importando los atributos personalizados"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "error importando los roles"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "error importando los miembros"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "error importando los sprints"
-#: taiga/export_import/dump_service.py:173
+#: 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:178
+#: 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:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "error importando las peticiones"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "error importando las tareas"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "error importando las etiquetas"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "error importando los timelines"
@@ -597,11 +597,11 @@ msgstr "Contiene attributos personalizados inválidos."
msgid "Name duplicated for the project"
msgstr "Nombre duplicado para el proyecto"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Erro generando el volcado de datos del proyecto"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Error cargando el volcado de datos del proyecto"
@@ -848,11 +848,12 @@ msgstr "Se requiere autenticación"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "nombre"
@@ -868,7 +869,7 @@ msgstr "web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descripción"
@@ -906,7 +907,7 @@ msgstr "comentario"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -939,7 +940,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Información extra"
@@ -1369,17 +1370,17 @@ msgstr "El ID de proyecto no coincide entre el adjunto y un proyecto"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1398,7 +1399,7 @@ msgstr "id de objeto"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1418,10 +1419,10 @@ msgstr "está desactualizado"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "orden"
@@ -1700,9 +1701,10 @@ msgid "Likes"
msgstr "Likes"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slug"
@@ -1714,8 +1716,8 @@ 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:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "está cerrada"
@@ -1814,27 +1816,27 @@ msgstr "total de sprints"
msgid "total story points"
msgstr "puntos de historia totales"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "panel de backlog activado"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "panel de kanban activado"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "panel de wiki activo"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "panel de peticiones activo"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "sistema de videoconferencia"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "datos extra de videoconferencia"
@@ -1850,7 +1852,7 @@ msgstr "permisos de anónimo"
msgid "user permissions"
msgstr "permisos de usuario"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "privado"
@@ -1911,67 +1913,67 @@ msgstr "actividad el último mes"
msgid "activity last year"
msgstr "actividad el último áño"
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "configuración de modulos"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "archivado"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "limite del trabajo en progreso"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "rol por defecto para el propietario"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "opciones por defecto"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "estatuas de historias"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "puntos"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "estatus de tareas"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "estados de petición"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "tipos de petición"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "prioridades"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "gravedades"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "roles"
@@ -3608,19 +3610,39 @@ msgstr "href"
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:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Información personal"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Permisos"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr "Restricciones"
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "datos importántes"
diff --git a/taiga/locale/fi/LC_MESSAGES/django.po b/taiga/locale/fi/LC_MESSAGES/django.po
index 1d97923b..42a4be30 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Finnish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fi/)\n"
@@ -505,59 +505,59 @@ msgstr "Tarvitaan tiedosto"
msgid "Invalid dump format"
msgstr "Virheellinen tiedostomuoto"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr "virhe projektidatan tuonnissa"
-#: taiga/export_import/dump_service.py:128
+#: taiga/export_import/dump_service.py:125
msgid "error importing lists of project attributes"
msgstr "virhe atribuuttilistan tuonnissa"
-#: taiga/export_import/dump_service.py:133
+#: taiga/export_import/dump_service.py:130
msgid "error importing default project attributes values"
msgstr "virhe oletusarvojen tuonnissa"
-#: taiga/export_import/dump_service.py:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "virhe omien arvojen tuonnissa"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "virhe roolien tuonnissa"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "virhe jäsenyyksien tuonnissa"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "virhe kierroksien tuonnissa"
-#: taiga/export_import/dump_service.py:173
+#: taiga/export_import/dump_service.py:170
msgid "error importing wiki pages"
msgstr "virhe wiki-sivujen tuonnissa"
-#: taiga/export_import/dump_service.py:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr "virhe viki-linkkien tuonnissa"
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "virhe pyyntöjen tuonnissa"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "virhe tehtävien tuonnissa"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "virhe avainsanojen sisäänlukemisessa"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "virhe aikajanojen tuonnissa"
@@ -580,11 +580,11 @@ msgstr "Sisältää vieheellisiä omia kenttiä."
msgid "Name duplicated for the project"
msgstr "Nimi on tuplana projektille"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Virhe tiedoston luonnissa"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Virhe tiedoston latauksessa"
@@ -828,11 +828,12 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "nimi"
@@ -848,7 +849,7 @@ msgstr ""
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "kuvaus"
@@ -886,7 +887,7 @@ msgstr "kommentti"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -921,7 +922,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Lisätiedot"
@@ -1326,17 +1327,17 @@ msgstr "Projekti ID ei vastaa kohdetta ja projektia"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1355,7 +1356,7 @@ msgstr "objekti ID"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1375,10 +1376,10 @@ msgstr "on poistettu"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "order"
@@ -1657,9 +1658,10 @@ msgid "Likes"
msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "hukka-aika"
@@ -1671,8 +1673,8 @@ msgstr "arvioitu alkupvm"
msgid "estimated finish date"
msgstr "arvioitu loppupvm"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "on suljettu"
@@ -1769,27 +1771,27 @@ msgstr "virstapyväitä yhteensä"
msgid "total story points"
msgstr "käyttäjätarinan yhteispisteet"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "aktiivinen odottavien paneeli"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "aktiivinen kanban-paneeli"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "aktiivinen wiki-paneeli"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "aktiivinen pyyntöpaneeli"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "videokokous järjestelmä"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr ""
@@ -1805,7 +1807,7 @@ msgstr "vieraan oikeudet"
msgid "user permissions"
msgstr "käyttäjän oikeudet"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "on yksityinen"
@@ -1866,67 +1868,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "moduulien asetukset"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "on arkistoitu"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "työn alla olevien max"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "arvo"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "oletus omistajan rooli"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "oletus optiot"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "kt tilat"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "pisteet"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "tehtävän tilat"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "pyyntöjen tilat"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "pyyntötyypit"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "kiireellisyydet"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "vakavuudet"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "roolit"
@@ -3545,19 +3547,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Henkilökohtaiset tiedot"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Oikeudet"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Tärkeät päivämäärät"
diff --git a/taiga/locale/fr/LC_MESSAGES/django.po b/taiga/locale/fr/LC_MESSAGES/django.po
index 9533481a..f84c878e 100644
--- a/taiga/locale/fr/LC_MESSAGES/django.po
+++ b/taiga/locale/fr/LC_MESSAGES/django.po
@@ -22,8 +22,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: French (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/fr/)\n"
@@ -539,60 +539,60 @@ msgstr "Fichier de dump obligatoire"
msgid "Invalid dump format"
msgstr "Format de dump invalide"
-#: taiga/export_import/dump_service.py:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: 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:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "Erreur à l'importation des rôles"
-#: taiga/export_import/dump_service.py:163
+#: 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:168
+#: 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:173
+#: 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:178
+#: 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:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "erreur à l'importation des problèmes"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: 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:198
+#: 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:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "erreur lors de l'import des timelines"
@@ -615,11 +615,11 @@ 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:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Erreur dans la génération du dump du projet"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Erreur au chargement du dump du projet"
@@ -852,11 +852,12 @@ msgstr "Authentification requise"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "nom"
@@ -872,7 +873,7 @@ msgstr "web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "description"
@@ -910,7 +911,7 @@ msgstr "Commentaire"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -943,7 +944,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informations supplémentaires"
@@ -1339,17 +1340,17 @@ msgstr "L'identifiant du projet de correspond pas entre l'objet et le projet"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1368,7 +1369,7 @@ msgstr "identifiant de l'objet"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1388,10 +1389,10 @@ msgstr "est obsolète"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "ordre"
@@ -1670,9 +1671,10 @@ msgid "Likes"
msgstr "Aime"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slug"
@@ -1684,8 +1686,8 @@ 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:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "est fermé"
@@ -1782,27 +1784,27 @@ msgstr "total des jalons"
msgid "total story points"
msgstr "total des points d'histoire"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "panneau backlog actif"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "panneau kanban actif"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "panneau wiki actif"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "panneau problèmes actif"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "plateforme de vidéoconférence"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "données complémentaires pour la salle de vidéoconférence"
@@ -1818,7 +1820,7 @@ msgstr "Permissions anonymes"
msgid "user permissions"
msgstr "Permission de l'utilisateur"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "est privé"
@@ -1879,67 +1881,67 @@ msgstr "activité du mois écoulé"
msgid "activity last year"
msgstr "activité de l'année écoulée"
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "Configurations des modules"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "est archivé"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "limite de travail en cours"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "valeur"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "rôle par défaut du propriétaire"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "options par défaut"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "statuts des us"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "points"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "états des tâches"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "statuts des problèmes"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "types de problèmes"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "priorités"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "sévérités"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "rôles"
@@ -3332,19 +3334,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Informations personnelles"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Permissions"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Dates importantes"
diff --git a/taiga/locale/it/LC_MESSAGES/django.po b/taiga/locale/it/LC_MESSAGES/django.po
index 1054bf89..c1f4fcd6 100644
--- a/taiga/locale/it/LC_MESSAGES/django.po
+++ b/taiga/locale/it/LC_MESSAGES/django.po
@@ -3,6 +3,7 @@
# This file is distributed under the same license as the taiga-back package.
#
# Translators:
+# Alberto Gloder , 2016
# Andrea Raimondi , 2015
# David Barragán , 2015
# F B , 2016
@@ -14,8 +15,8 @@ msgid ""
msgstr ""
"Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-04-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Italian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/it/)\n"
@@ -27,30 +28,30 @@ msgstr ""
#: taiga/auth/api.py:100
msgid "Public register is disabled."
-msgstr "Registro pubblico disabilitato"
+msgstr "La registrazione pubblica è disabilitata."
#: taiga/auth/api.py:133
msgid "invalid register type"
-msgstr "Tipo di registro invalido"
+msgstr "Tipo di registrazione non valida"
#: taiga/auth/api.py:146
msgid "invalid login type"
-msgstr "Tipo di login invalido"
+msgstr "Tipo di login non valido"
#: taiga/auth/serializers.py:35 taiga/users/serializers.py:64
msgid "invalid username"
-msgstr "Username non valido"
+msgstr "Nome utente non valido"
#: taiga/auth/serializers.py:40 taiga/users/serializers.py:70
msgid ""
"Required. 255 characters or fewer. Letters, numbers and /./-/_ characters'"
msgstr ""
-"Sono richiesti 255 caratteri, o meno, contenenti: lettere, numeri e "
+"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 appena scelto è già utilizzato."
+msgstr "Il nome utente scelto è già utilizzato."
#: taiga/auth/services.py:78
msgid "Email is already in use."
@@ -58,7 +59,7 @@ 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"
+msgstr "Il token non corrisponde a nessun invito valido."
#: taiga/auth/services.py:122
msgid "User is already registered."
@@ -70,7 +71,7 @@ msgstr "Questo utente fa già parte del progetto."
#: taiga/auth/services.py:172
msgid "Error on creating new user."
-msgstr "Errore nella creazione dell'utente."
+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
@@ -89,19 +90,20 @@ msgstr "Valore non valido."
#: taiga/base/api/fields.py:477
#, python-format
msgid "'%s' value must be either True or False."
-msgstr "il valore di '%s' deve essere o vero o falso."
+msgstr "il valore di '%s' deve essere o Vero o Falso."
#: taiga/base/api/fields.py:541
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"
+"Uno 'slug' valido è composto da lettere, numeri, caratteri di sottolineatura "
+"o trattini."
#: taiga/base/api/fields.py:556
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
-msgstr "Seleziona un valore valido. %(value)s non è una scelta disponibile."
+msgstr ""
+"Seleziona una scelta valida. %(value)s non è fra le scelte disponibili."
#: taiga/base/api/fields.py:619
msgid "Enter a valid email address."
@@ -120,21 +122,21 @@ msgstr "L'orario non ha un formato valido. Usa uno dei formati disponibili: %s"
#: taiga/base/api/fields.py:795
#, python-format
msgid "Time has wrong format. Use one of these formats instead: %s"
-msgstr "Formato temporale errato. Usare uno dei seguenti formati: %s"
+msgstr "Formato temporale errato. Usa uno dei seguenti formati: %s"
#: taiga/base/api/fields.py:852
msgid "Enter a whole number."
-msgstr "Inserire il numero completo."
+msgstr "Inserisci il numero completo."
#: taiga/base/api/fields.py:853 taiga/base/api/fields.py:906
#, python-format
msgid "Ensure this value is less than or equal to %(limit_value)s."
-msgstr "Assicurati che il valore sia minore o uguale a %(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
#, python-format
msgid "Ensure this value is greater than or equal to %(limit_value)s."
-msgstr "Assicurati che il valore sia maggiore o uguale a %(limit_value)s."
+msgstr "Assicurati che questo valore sia maggiore o uguale di %(limit_value)s."
#: taiga/base/api/fields.py:884
#, python-format
@@ -143,7 +145,7 @@ msgstr "il valore \"%s\" deve essere un valore \"float\"."
#: taiga/base/api/fields.py:905
msgid "Enter a number."
-msgstr "Inserisci un numero"
+msgstr "Inserisci un numero."
#: taiga/base/api/fields.py:908
#, python-format
@@ -163,7 +165,7 @@ msgstr "Assicurati che non ci siano più di %s cifre prima del punto decimale."
#: taiga/base/api/fields.py:977
msgid "No file was submitted. Check the encoding type on the form."
msgstr ""
-"Non è stato caricato nessun file. Controlla il tipo di codifica nella scheda."
+"Non è stato caricato alcun file. Controlla il tipo di codifica nella scheda."
#: taiga/base/api/fields.py:978
msgid "No file was submitted."
@@ -178,7 +180,7 @@ msgstr "Il file caricato è vuoto."
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
msgstr ""
-"Assicurati che il nome del file abbiamo al massimo %(max)d caratteri (ne ha "
+"Assicurati che il nome del file abbia al massimo %(max)d caratteri (ne ha "
"%(length)d)."
#: taiga/base/api/fields.py:981
@@ -529,60 +531,60 @@ msgstr "E' richiesto un file di dump"
msgid "Invalid dump format"
msgstr "Formato di dump invalido"
-#: taiga/export_import/dump_service.py:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: 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:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "Errore nell'importazione i ruoli"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "Errore nell'importazione delle iscrizioni"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "errore nell'importazione degli sprints"
-#: taiga/export_import/dump_service.py:173
+#: 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:178
+#: 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:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "errore nell'importazione dei problemi"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "Errore nell'importazione dei compiti "
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "Errore nell'importazione dei tags"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "Errore nell'importazione delle timelines"
@@ -605,11 +607,11 @@ msgstr "Contiene campi personalizzati invalidi."
msgid "Name duplicated for the project"
msgstr "Il nome del progetto è duplicato"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Errore nella creazione del dump di progetto"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Errore nel caricamento del dump di progetto"
@@ -915,11 +917,12 @@ msgstr "E' richiesta l'autenticazione"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "nome"
@@ -935,7 +938,7 @@ msgstr "web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descrizione"
@@ -973,7 +976,7 @@ msgstr "Commento"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -1006,7 +1009,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informazioni aggiuntive"
@@ -1460,17 +1463,17 @@ msgstr "L'ID di progetto non corrisponde tra oggetto e progetto"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1489,7 +1492,7 @@ msgstr "ID dell'oggetto"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1509,10 +1512,10 @@ msgstr "non approvato"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "ordine"
@@ -1791,9 +1794,10 @@ msgid "Likes"
msgstr "Piaciuto"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "lumaca"
@@ -1805,8 +1809,8 @@ msgstr "data stimata di inizio"
msgid "estimated finish date"
msgstr "data stimata di fine"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "è concluso"
@@ -1904,27 +1908,27 @@ msgstr "tappe totali"
msgid "total story points"
msgstr "punti totali della storia"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "pannello di backlog attivo"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "pannello kanban attivo"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "pannello wiki attivo"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "pannello dei problemi attivo"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "sistema di videoconferenza"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "ulteriori dati di videoconferenza "
@@ -1940,7 +1944,7 @@ msgstr "permessi anonimi"
msgid "user permissions"
msgstr "permessi dell'utente"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "è privato"
@@ -2001,67 +2005,67 @@ msgstr "attività nel mese"
msgid "activity last year"
msgstr "attività nell'anno"
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "configurazione dei moduli"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "è archivitato"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "limite dei lavori in corso"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "valore"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "ruolo proprietario predefinito"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "opzioni predefinite "
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "stati della storia utente"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "punti"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "stati del compito"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "stati del probema"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "tipologie del problema"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "priorità"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "criticità "
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "ruoli"
@@ -3291,11 +3295,15 @@ msgid ""
"new project owner for \"%(project_name)s\".
\n"
" "
msgstr ""
+"\n"
+"Ciao %(old_owner_name)s,
\n"
+"%(new_owner_name)s ha accettato la tua offerta e diventerà il nuovo "
+"proprietario del progetto \"%(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 dice:
"
#: taiga/projects/templates/emails/transfer_accept-body-html.jinja:14
msgid ""
@@ -3304,6 +3312,9 @@ msgid ""
"p>\n"
" "
msgstr ""
+"\n"
+"Da adesso in avanti, il tuo nuovo status per questo progetto sarà \"admin"
+"\".
"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:1
#, python-format
@@ -3313,6 +3324,10 @@ msgid ""
"%(new_owner_name)s has accepted your offer and will become the new project "
"owner for \"%(project_name)s\".\n"
msgstr ""
+"\n"
+"Ciao %(old_owner_name)s,\n"
+"%(new_owner_name)s ha accettato la tua offerta e diventerà il nuovo "
+"proprietario del progetto \"%(project_name)s\".\n"
#: taiga/projects/templates/emails/transfer_accept-body-text.jinja:7
#, python-format
@@ -3843,19 +3858,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr "Controlla le API della storie per la differenza esatta"
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Informazioni personali"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Permessi"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Date importanti"
diff --git a/taiga/locale/nl/LC_MESSAGES/django.po b/taiga/locale/nl/LC_MESSAGES/django.po
index e2543498..d1782718 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Dutch (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/nl/)\n"
@@ -518,59 +518,59 @@ msgstr "Dump file nodig"
msgid "Invalid dump format"
msgstr "Ongeldig dump formaat"
-#: taiga/export_import/dump_service.py:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "fout bij importeren eigen attributen"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "fout bij importeren rollen"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "fout bij importeren lidmaatschappen"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "fout bij importeren sprints"
-#: taiga/export_import/dump_service.py:173
+#: 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:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr "fout bij importeren wiki links"
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "fout bij importeren issues"
-#: taiga/export_import/dump_service.py:188
+#: taiga/export_import/dump_service.py:185
msgid "error importing user stories"
msgstr "fout bij importeren user stories"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "fout bij importeren taken"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "fout bij importeren tags"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "fout bij importeren tijdlijnen"
@@ -593,11 +593,11 @@ msgstr "Het bevat ongeldige eigen velden:"
msgid "Name duplicated for the project"
msgstr "Naam gedupliceerd voor het project"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Fout bij genereren project dump"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Fout bij laden project dump"
@@ -778,11 +778,12 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "naam"
@@ -798,7 +799,7 @@ msgstr ""
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "omschrijving"
@@ -836,7 +837,7 @@ msgstr "commentaar"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -870,7 +871,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Extra info"
@@ -1260,17 +1261,17 @@ msgstr "Project ID van object is niet gelijk aan die van het project"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1289,7 +1290,7 @@ msgstr "object id"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1309,10 +1310,10 @@ msgstr "is verouderd"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "volgorde"
@@ -1593,9 +1594,10 @@ msgid "Likes"
msgstr "Personen die dit leuk vinden"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slug"
@@ -1607,8 +1609,8 @@ msgstr "geschatte start datum"
msgid "estimated finish date"
msgstr "geschatte datum van afwerking"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "is gesloten"
@@ -1705,27 +1707,27 @@ msgstr "totaal van de milestones"
msgid "total story points"
msgstr "totaal story points"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "actief backlog paneel"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "actief kanban paneel"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "actief wiki paneel"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "actief issues paneel"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "videoconference systeem"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr ""
@@ -1741,7 +1743,7 @@ msgstr "anonieme toestemmingen"
msgid "user permissions"
msgstr "gebruikers toestemmingen"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "is privé"
@@ -1802,67 +1804,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "module config"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "is gearchiveerd"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "work in progress limiet"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "waarde"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "standaard rol eigenaar"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "standaard instellingen"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "us statussen"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "punten"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "taak statussen"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "issue statussen"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "issue types"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "prioriteiten"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "ernstniveaus"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "rollen"
@@ -3240,19 +3242,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Persoonlijke info"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Toestemmingen"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Belangrijke data"
diff --git a/taiga/locale/pl/LC_MESSAGES/django.po b/taiga/locale/pl/LC_MESSAGES/django.po
index f205e65f..5ba1251c 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Polish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/pl/)\n"
@@ -519,59 +519,59 @@ msgstr "Wymagany plik zrzutu"
msgid "Invalid dump format"
msgstr "Nieprawidłowy format zrzutu"
-#: taiga/export_import/dump_service.py:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: 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:148
+#: 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:163
+#: 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:168
+#: 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:173
+#: 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:178
+#: 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:183
+#: 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:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "błąd w trakcie importu zadań"
-#: taiga/export_import/dump_service.py:198
+#: 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:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "błąd w trakcie importu osi czasu"
@@ -594,11 +594,11 @@ msgstr "Zawiera niewłaściwe pola niestandardowe."
msgid "Name duplicated for the project"
msgstr "Nazwa projektu zduplikowana"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Błąd w trakcie generowania zrzutu projektu"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Błąd w trakcie wczytywania zrzutu projektu"
@@ -846,11 +846,12 @@ msgstr ""
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "nazwa"
@@ -866,7 +867,7 @@ msgstr "web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "opis"
@@ -904,7 +905,7 @@ msgstr "komentarz"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -938,7 +939,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Dodatkowe info"
@@ -1368,17 +1369,17 @@ msgstr "ID nie pasuje pomiędzy obiektem a projektem"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1397,7 +1398,7 @@ msgstr "id obiektu"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1417,10 +1418,10 @@ msgstr "jest przestarzałe"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "kolejność"
@@ -1699,9 +1700,10 @@ msgid "Likes"
msgstr ""
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slug"
@@ -1713,8 +1715,8 @@ msgstr "szacowana data rozpoczecia"
msgid "estimated finish date"
msgstr "szacowana data zakończenia"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "jest zamknięte"
@@ -1811,27 +1813,27 @@ msgstr "wszystkich kamieni milowych"
msgid "total story points"
msgstr "wszystkich punktów "
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "aktywny panel backlog"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "aktywny panel Kanban"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "aktywny panel Wiki"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "aktywny panel zgłoszeń "
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "system wideokonferencji"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "dodatkowe dane dla wideokonferencji"
@@ -1847,7 +1849,7 @@ msgstr "uprawnienia anonimowych"
msgid "user permissions"
msgstr "uprawnienia użytkownika"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "jest prywatna"
@@ -1908,67 +1910,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "konfiguracja modułów"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "zarchiwizowane"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "limit postępu prac"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "wartość"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "domyśla rola właściciela"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "domyślne opcje"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "statusy HU"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "pinkty"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "statusy zadań"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "statusy zgłoszeń"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "typy zgłoszeń"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "priorytety"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "ważność"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "role"
@@ -3607,19 +3609,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr "Dla pełengo diffa sprawdź API historii"
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Informacje osobiste"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Uprawnienia"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Ważne daty"
diff --git a/taiga/locale/pt_BR/LC_MESSAGES/django.po b/taiga/locale/pt_BR/LC_MESSAGES/django.po
index 41b4e7b3..9b3d2b72 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+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"
@@ -527,59 +527,59 @@ 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:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "erro importando atributos personalizados"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "erro importando funcões"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "erro importando filiações"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "erro importando sprints"
-#: taiga/export_import/dump_service.py:173
+#: taiga/export_import/dump_service.py:170
msgid "error importing wiki pages"
msgstr "erro importando páginas wiki"
-#: taiga/export_import/dump_service.py:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr "erro importando wiki links"
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "erro importando casos"
-#: taiga/export_import/dump_service.py:188
+#: taiga/export_import/dump_service.py:185
msgid "error importing user stories"
msgstr "erro importando user stories"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "erro importando tarefas"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "erro importando tags"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "erro importando linha do tempo"
@@ -602,11 +602,11 @@ msgstr "Contém campos personalizados inválidos"
msgid "Name duplicated for the project"
msgstr "Nome duplicado para o projeto"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Erro gerando arquivo de restauração do projeto"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Erro carregando arquivo de restauração do projeto"
@@ -853,11 +853,12 @@ msgstr "Autenticação necessária"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "Nome"
@@ -873,7 +874,7 @@ msgstr "web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "descrição"
@@ -911,7 +912,7 @@ msgstr "comentário"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -945,7 +946,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Informação extra"
@@ -1374,17 +1375,17 @@ msgstr "ID do projeto não combina entre objeto e projeto"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1403,7 +1404,7 @@ msgstr "identidade de objeto"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1423,10 +1424,10 @@ msgstr "está obsoleto"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "ordem"
@@ -1705,9 +1706,10 @@ msgid "Likes"
msgstr "Curtidas"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slug"
@@ -1719,8 +1721,8 @@ 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:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "está fechado"
@@ -1817,27 +1819,27 @@ msgstr "total de marcos de progresso"
msgid "total story points"
msgstr "pontos totais de US"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "painel de backlog ativo"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "painel de kanban ativo"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "painel de wiki ativo"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "painel de casos ativo"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "sistema de vídeo conferência"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "informação extra de vídeo conferência"
@@ -1853,7 +1855,7 @@ msgstr "permissão anônima"
msgid "user permissions"
msgstr "permissão de usuário"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "é privado"
@@ -1914,67 +1916,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "configurações de módulos"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "está arquivado"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "trabalho no limite de progresso"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "valor"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "função padrão para dono "
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "opções padrão"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "status de US"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "pontos"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "status de tarefa"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "status de casos"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "tipos de caso"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "prioridades"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "severidades"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "funções"
@@ -3588,19 +3590,39 @@ msgstr "href"
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:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Informação pessoal"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Permissões"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Datas importantes"
diff --git a/taiga/locale/ru/LC_MESSAGES/django.po b/taiga/locale/ru/LC_MESSAGES/django.po
index ddcdfa3c..f9c67e5f 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Russian (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/ru/)\n"
@@ -529,59 +529,59 @@ msgstr "Необходим дамп-файл"
msgid "Invalid dump format"
msgstr "Неправильный формат дампа"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr "ошибка при импорте данных проекта"
-#: taiga/export_import/dump_service.py:128
+#: taiga/export_import/dump_service.py:125
msgid "error importing lists of project attributes"
msgstr "ошибка при импорте списков свойств проекта"
-#: taiga/export_import/dump_service.py:133
+#: taiga/export_import/dump_service.py:130
msgid "error importing default project attributes values"
msgstr "ошибка при импорте значений по умолчанию свойств проекта"
-#: taiga/export_import/dump_service.py:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "ошибка при импорте пользовательских свойств"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "ошибка при импорте ролей"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "ошибка при импорте членства"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "ошибка при импорте спринтов"
-#: taiga/export_import/dump_service.py:173
+#: taiga/export_import/dump_service.py:170
msgid "error importing wiki pages"
msgstr "ошибка при импорте вики-страниц"
-#: taiga/export_import/dump_service.py:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr "ошибка при импорте вики-ссылок"
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "ошибка при импорте запросов"
-#: taiga/export_import/dump_service.py:188
+#: taiga/export_import/dump_service.py:185
msgid "error importing user stories"
msgstr "ошибка импорта историй от пользователей"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "ошибка импорта задач"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "ошибка импорта тэгов"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "ошибка импорта хронологии проекта"
@@ -604,11 +604,11 @@ msgstr "Содержит неверные специальные поля"
msgid "Name duplicated for the project"
msgstr "Уже есть такое имя для проекта"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Ошибка создания свалочного файла для проекта"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Ошибка загрузки свалочного файла проекта"
@@ -853,11 +853,12 @@ msgstr "Необходима аутентификация"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "имя"
@@ -873,7 +874,7 @@ msgstr "веб"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "описание"
@@ -911,7 +912,7 @@ msgstr "комментарий"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -945,7 +946,7 @@ msgstr ""
" "
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Дополнительное инфо"
@@ -1375,17 +1376,17 @@ msgstr "Идентификатор проекта не подходит к эт
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1404,7 +1405,7 @@ msgstr "идентификатор объекта"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1424,10 +1425,10 @@ msgstr "устаревшее"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "порядок"
@@ -1710,9 +1711,10 @@ msgid "Likes"
msgstr "Лайки"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "ссылочное имя"
@@ -1724,8 +1726,8 @@ msgstr "предполагаемая дата начала"
msgid "estimated finish date"
msgstr "предполагаемая дата завершения"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "закрыто"
@@ -1824,27 +1826,27 @@ msgstr "общее количество вех"
msgid "total story points"
msgstr "очки истории"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "активная панель списка задач"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "активная панель kanban"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "активная wiki-панель"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "панель активных запросов"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "система видеоконференций"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "дополнительные данные системы видеоконференций"
@@ -1860,7 +1862,7 @@ msgstr "права анонимов"
msgid "user permissions"
msgstr "права пользователя"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "личное"
@@ -1921,67 +1923,67 @@ msgstr "активность за месяц"
msgid "activity last year"
msgstr "активность за год"
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "конфигурация модулей"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "архивировано"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "ограничение на активную работу"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "значение"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "роль владельца по умолчанию"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "параметры по умолчанию"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "статусы ПИ"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "очки"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "статусы задач"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "статусы запросов"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "типы запросов"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "приоритеты"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "степени важности"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "роли"
@@ -3609,19 +3611,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr "Свертесть с историей API для получения изменений"
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Личные данные"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Права доступа"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Важные даты"
diff --git a/taiga/locale/sv/LC_MESSAGES/django.po b/taiga/locale/sv/LC_MESSAGES/django.po
index d217159d..88143ba9 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Swedish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/sv/)\n"
@@ -503,59 +503,59 @@ msgstr "Behöver en hämtningsfil"
msgid "Invalid dump format"
msgstr "Invalid hämtningsfilformat"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr "fel vid import av projektdata"
-#: taiga/export_import/dump_service.py:128
+#: 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:133
+#: 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:143
+#: 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:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "fel vid importering av roller"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "fel vid import av medlemskap"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "felaktig import av sprintar"
-#: taiga/export_import/dump_service.py:173
+#: 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:178
+#: 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:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "fel vid import av ärenden"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "fel vid import av uppgifter"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "fel vid importering av taggar"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "fel vid importering av tidslinje"
@@ -578,11 +578,11 @@ 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:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Fel vid skapandet av projektkopia"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Feil vid hämtning av projektkopia"
@@ -741,11 +741,12 @@ msgstr "Verifiering krävs"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "namn"
@@ -761,7 +762,7 @@ msgstr "Internet"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "beskrivning"
@@ -799,7 +800,7 @@ msgstr "kommentera"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -825,7 +826,7 @@ msgid ""
msgstr ""
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Extra information"
@@ -1215,17 +1216,17 @@ msgstr "Projekt-ID stämmer inte mellan objekt och projekt"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1244,7 +1245,7 @@ msgstr "objekt-ID"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1264,10 +1265,10 @@ msgstr "undviks"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "sortera"
@@ -1546,9 +1547,10 @@ msgid "Likes"
msgstr "Gillar"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "slugg"
@@ -1560,8 +1562,8 @@ msgstr "Beräknad startdatum"
msgid "estimated finish date"
msgstr "Beräknad slutdato"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "är stängd"
@@ -1658,27 +1660,27 @@ msgstr "totalt antal milstolpar"
msgid "total story points"
msgstr "totalt antal historiepoäng"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "aktivt panel för inkorg"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "aktiv kanban-panel"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "aktiv wiki-panel"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "aktiv panel för ärenden"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "videokonferensssystem"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "videokonferens - extra data"
@@ -1694,7 +1696,7 @@ msgstr "anonyma rättigheter"
msgid "user permissions"
msgstr "användarbehörigheter"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "är privat"
@@ -1755,67 +1757,67 @@ msgstr ""
msgid "activity last year"
msgstr ""
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "konfigurera moduler"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "är arkiverad"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "begränsad arbete pågår"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "värde"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "ägarens standardroll"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "standard val"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "US statuser"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "poäng"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "statuser för uppgifter"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "status för ärenden"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "ärendentyper"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "prioriteter"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "allvarsgrad"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "roller"
@@ -3152,19 +3154,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr "Kolla historie API för exakt skillnad"
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Personalinformation"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "Behörigheter"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Viktiga datum"
diff --git a/taiga/locale/tr/LC_MESSAGES/django.po b/taiga/locale/tr/LC_MESSAGES/django.po
index fe7cf855..59c3af70 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+0000\n"
"Last-Translator: Taiga Dev Team \n"
"Language-Team: Turkish (http://www.transifex.com/taiga-agile-llc/taiga-back/"
"language/tr/)\n"
@@ -513,59 +513,59 @@ 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:115
+#: 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:128
+#: 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:133
+#: 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:143
+#: 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:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "İçeri aktarılan rollerde hata"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "İçeri aktarılan üyeliklerde hata"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "İçeri aktarılan sprintlerde hata"
-#: taiga/export_import/dump_service.py:173
+#: 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:178
+#: 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:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "İçeri aktarılan taleplerde hata"
-#: taiga/export_import/dump_service.py:188
+#: 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:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "İçeri aktarılan görevlerde hata"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "İçeri aktarılan etiketlerde hata"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "zaman çizelgesi içeri aktarılırken hata"
@@ -588,11 +588,11 @@ msgstr "Geçersiz özel alanlar içeriyor."
msgid "Name duplicated for the project"
msgstr "Aynı isimde proje bulunmakta"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "Proje dökümü oluşturulurken hata"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "Proje dökümü yükleniyorken hata"
@@ -836,11 +836,12 @@ msgstr "Kimlik doğrulama gerekli"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "isim"
@@ -856,7 +857,7 @@ msgstr "web"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "tanı"
@@ -894,7 +895,7 @@ msgstr "yorum"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -926,7 +927,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "Ekstra bilgi"
@@ -1322,17 +1323,17 @@ msgstr "Proje ve nesne arasında Proje ID uyuşmazlığı mevcut"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1351,7 +1352,7 @@ msgstr "nesne id"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1371,10 +1372,10 @@ msgstr "kaldırıldı"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "sıra"
@@ -1653,9 +1654,10 @@ msgid "Likes"
msgstr "Beğeniler"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "satır"
@@ -1667,8 +1669,8 @@ 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:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "kapatılmış"
@@ -1765,27 +1767,27 @@ msgstr "aşamaların toplamı"
msgid "total story points"
msgstr "toplam hikaye puanı"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "aktif birikmiş iler paneli"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "aktif kanban paneli"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "aktif wiki paneli"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "aktif talep paneli"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "video konferans sistemi"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "videokonferans ekstra verisi"
@@ -1801,7 +1803,7 @@ msgstr "anonim izinler"
msgid "user permissions"
msgstr "kullanıcı izinleri"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "gizli"
@@ -1862,67 +1864,67 @@ msgstr "geçen ayın aktiviteleri"
msgid "activity last year"
msgstr "geçen yılın aktiviteleri"
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "modül ayarları"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "arşivlenmiş"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr ""
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "değer"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "varsayılan sahip rolü"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "varsayılan ayarlar"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "kh durumları"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "puanlar"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "görev durumları"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "talep durumları"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "talep tipleri"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "öncelikler"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "önem durumları"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "roller"
@@ -3324,19 +3326,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr ""
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "Kişisel bilgi"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "İzinler"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "Önemli tarihler"
diff --git a/taiga/locale/zh-Hant/LC_MESSAGES/django.po b/taiga/locale/zh-Hant/LC_MESSAGES/django.po
index 0734e55d..d357abfa 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-08 13:23+0200\n"
-"PO-Revision-Date: 2016-04-08 11:23+0000\n"
+"POT-Creation-Date: 2016-04-19 16:00+0200\n"
+"PO-Revision-Date: 2016-04-19 14:00+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"
@@ -510,59 +510,59 @@ msgstr "需要的堆存檔案"
msgid "Invalid dump format"
msgstr "無效堆存格式"
-#: taiga/export_import/dump_service.py:115
+#: taiga/export_import/dump_service.py:112
msgid "error importing project data"
msgstr "滙入重要專案資料出錯"
-#: taiga/export_import/dump_service.py:128
+#: taiga/export_import/dump_service.py:125
msgid "error importing lists of project attributes"
msgstr "滙入標籤出錯"
-#: taiga/export_import/dump_service.py:133
+#: taiga/export_import/dump_service.py:130
msgid "error importing default project attributes values"
msgstr "滙入預設專案屬性數值出錯"
-#: taiga/export_import/dump_service.py:143
+#: taiga/export_import/dump_service.py:140
msgid "error importing custom attributes"
msgstr "滙入客制性屬出錯"
-#: taiga/export_import/dump_service.py:148
+#: taiga/export_import/dump_service.py:145
msgid "error importing roles"
msgstr "滙入角色出錯"
-#: taiga/export_import/dump_service.py:163
+#: taiga/export_import/dump_service.py:160
msgid "error importing memberships"
msgstr "滙入成員資格出錯"
-#: taiga/export_import/dump_service.py:168
+#: taiga/export_import/dump_service.py:165
msgid "error importing sprints"
msgstr "滙入衝刺任務出錯"
-#: taiga/export_import/dump_service.py:173
+#: taiga/export_import/dump_service.py:170
msgid "error importing wiki pages"
msgstr "滙入維基頁出錯"
-#: taiga/export_import/dump_service.py:178
+#: taiga/export_import/dump_service.py:175
msgid "error importing wiki links"
msgstr "滙入維基連結出錯"
-#: taiga/export_import/dump_service.py:183
+#: taiga/export_import/dump_service.py:180
msgid "error importing issues"
msgstr "滙入問題出錯"
-#: taiga/export_import/dump_service.py:188
+#: taiga/export_import/dump_service.py:185
msgid "error importing user stories"
msgstr "滙入使用者故事出錯"
-#: taiga/export_import/dump_service.py:193
+#: taiga/export_import/dump_service.py:190
msgid "error importing tasks"
msgstr "滙入任務出錯"
-#: taiga/export_import/dump_service.py:198
+#: taiga/export_import/dump_service.py:195
msgid "error importing tags"
msgstr "滙入標籤出錯"
-#: taiga/export_import/dump_service.py:202
+#: taiga/export_import/dump_service.py:199
msgid "error importing timelines"
msgstr "滙入時間軸出錯"
@@ -585,11 +585,11 @@ msgstr "包括無效慣例欄位"
msgid "Name duplicated for the project"
msgstr "專案的名稱被複製了"
-#: taiga/export_import/tasks.py:54 taiga/export_import/tasks.py:55
+#: taiga/export_import/tasks.py:55 taiga/export_import/tasks.py:56
msgid "Error generating project dump"
msgstr "產生專案傾倒時出錯"
-#: taiga/export_import/tasks.py:86 taiga/export_import/tasks.py:87
+#: taiga/export_import/tasks.py:88 taiga/export_import/tasks.py:89
msgid "Error loading project dump"
msgstr "載入專案傾倒時出錯"
@@ -833,11 +833,12 @@ msgstr "要求取得授權"
#: taiga/external_apps/models.py:34
#: taiga/projects/custom_attributes/models.py:35
#: taiga/projects/milestones/models.py:38 taiga/projects/models.py:146
-#: taiga/projects/models.py:472 taiga/projects/models.py:511
-#: taiga/projects/models.py:536 taiga/projects/models.py:573
-#: taiga/projects/models.py:596 taiga/projects/models.py:619
-#: taiga/projects/models.py:654 taiga/projects/models.py:677
-#: taiga/users/models.py:292 taiga/webhooks/models.py:28
+#: 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
msgid "name"
msgstr "姓名"
@@ -853,7 +854,7 @@ msgstr "網頁"
#: 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:681 taiga/projects/tasks/models.py:61
+#: taiga/projects/models.py:687 taiga/projects/tasks/models.py:61
#: taiga/projects/userstories/models.py:92
msgid "description"
msgstr "描述"
@@ -891,7 +892,7 @@ msgstr "評論"
#: 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:683 taiga/projects/notifications/models.py:88
+#: 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
@@ -923,7 +924,7 @@ msgstr ""
"%(comment)s
"
#: taiga/feedback/templates/emails/feedback_notification-body-html.jinja:18
-#: taiga/users/admin.py:52
+#: taiga/users/admin.py:120
msgid "Extra info"
msgstr "額外資訊"
@@ -1348,17 +1349,17 @@ msgstr "專案ID不符合物件與專案"
#: 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/userstorage/models.py:26
+#: 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
-#: taiga/projects/models.py:460 taiga/projects/models.py:486
-#: taiga/projects/models.py:517 taiga/projects/models.py:546
-#: taiga/projects/models.py:579 taiga/projects/models.py:602
-#: taiga/projects/models.py:629 taiga/projects/models.py:660
+#: 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
@@ -1377,7 +1378,7 @@ msgstr "物件ID"
#: taiga/projects/attachments/models.py:50
#: 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:686
+#: 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
msgid "modified date"
@@ -1397,10 +1398,10 @@ msgstr "棄用"
#: taiga/projects/attachments/models.py:61
#: taiga/projects/custom_attributes/models.py:40
-#: taiga/projects/milestones/models.py:58 taiga/projects/models.py:476
-#: taiga/projects/models.py:513 taiga/projects/models.py:540
-#: taiga/projects/models.py:575 taiga/projects/models.py:598
-#: taiga/projects/models.py:623 taiga/projects/models.py:656
+#: 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
msgid "order"
msgstr "次序"
@@ -1679,9 +1680,10 @@ msgid "Likes"
msgstr "喜歡"
#: taiga/projects/milestones/models.py:41 taiga/projects/models.py:148
-#: taiga/projects/models.py:474 taiga/projects/models.py:538
-#: taiga/projects/models.py:621 taiga/projects/models.py:679
-#: taiga/projects/wiki/models.py:32 taiga/users/models.py:294
+#: 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
msgid "slug"
msgstr "代稱"
@@ -1693,8 +1695,8 @@ msgstr "预計開始日期"
msgid "estimated finish date"
msgstr "預計完成日期"
-#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:478
-#: taiga/projects/models.py:542 taiga/projects/models.py:625
+#: taiga/projects/milestones/models.py:54 taiga/projects/models.py:484
+#: taiga/projects/models.py:548 taiga/projects/models.py:631
msgid "is closed"
msgstr "被關閉"
@@ -1791,27 +1793,27 @@ msgstr "全部里程碑"
msgid "total story points"
msgstr "全部故事點數"
-#: taiga/projects/models.py:171 taiga/projects/models.py:692
+#: taiga/projects/models.py:171 taiga/projects/models.py:698
msgid "active backlog panel"
msgstr "活躍的待辦任務優先表面板"
-#: taiga/projects/models.py:173 taiga/projects/models.py:694
+#: taiga/projects/models.py:173 taiga/projects/models.py:700
msgid "active kanban panel"
msgstr "活躍的看板式面板"
-#: taiga/projects/models.py:175 taiga/projects/models.py:696
+#: taiga/projects/models.py:175 taiga/projects/models.py:702
msgid "active wiki panel"
msgstr "活躍的維基面板"
-#: taiga/projects/models.py:177 taiga/projects/models.py:698
+#: taiga/projects/models.py:177 taiga/projects/models.py:704
msgid "active issues panel"
msgstr "活躍的問題面板"
-#: taiga/projects/models.py:180 taiga/projects/models.py:701
+#: taiga/projects/models.py:180 taiga/projects/models.py:707
msgid "videoconference system"
msgstr "視訊會議系統"
-#: taiga/projects/models.py:182 taiga/projects/models.py:703
+#: taiga/projects/models.py:182 taiga/projects/models.py:709
msgid "videoconference extra data"
msgstr "視訊會議額外資料"
@@ -1827,7 +1829,7 @@ msgstr "匿名權限"
msgid "user permissions"
msgstr "使用者權限"
-#: taiga/projects/models.py:198
+#: taiga/projects/models.py:198 taiga/users/admin.py:61
msgid "is private"
msgstr "私密"
@@ -1888,67 +1890,67 @@ msgstr "上月活躍成員"
msgid "activity last year"
msgstr "去年活躍成員"
-#: taiga/projects/models.py:461
+#: taiga/projects/models.py:467
msgid "modules config"
msgstr "模組設定"
-#: taiga/projects/models.py:480
+#: taiga/projects/models.py:486
msgid "is archived"
msgstr "已歸檔"
-#: taiga/projects/models.py:482 taiga/projects/models.py:544
-#: taiga/projects/models.py:577 taiga/projects/models.py:600
-#: taiga/projects/models.py:627 taiga/projects/models.py:658
+#: 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:484
+#: taiga/projects/models.py:490
msgid "work in progress limit"
msgstr "工作進度限制"
-#: taiga/projects/models.py:515 taiga/userstorage/models.py:32
+#: taiga/projects/models.py:521 taiga/userstorage/models.py:32
msgid "value"
msgstr "價值"
-#: taiga/projects/models.py:689
+#: taiga/projects/models.py:695
msgid "default owner's role"
msgstr "預設所有者角色"
-#: taiga/projects/models.py:705
+#: taiga/projects/models.py:711
msgid "default options"
msgstr "預設選項"
-#: taiga/projects/models.py:706
+#: taiga/projects/models.py:712
msgid "us statuses"
msgstr "我們狀況"
-#: taiga/projects/models.py:707 taiga/projects/userstories/models.py:42
+#: taiga/projects/models.py:713 taiga/projects/userstories/models.py:42
#: taiga/projects/userstories/models.py:74
msgid "points"
msgstr "點數"
-#: taiga/projects/models.py:708
+#: taiga/projects/models.py:714
msgid "task statuses"
msgstr "任務狀況"
-#: taiga/projects/models.py:709
+#: taiga/projects/models.py:715
msgid "issue statuses"
msgstr "問題狀況"
-#: taiga/projects/models.py:710
+#: taiga/projects/models.py:716
msgid "issue types"
msgstr "問題類型"
-#: taiga/projects/models.py:711
+#: taiga/projects/models.py:717
msgid "priorities"
msgstr "優先性"
-#: taiga/projects/models.py:712
+#: taiga/projects/models.py:718
msgid "severities"
msgstr "嚴重性"
-#: taiga/projects/models.py:713
+#: taiga/projects/models.py:719
msgid "roles"
msgstr "角色"
@@ -3564,19 +3566,39 @@ msgstr "href"
msgid "Check the history API for the exact diff"
msgstr "檢查API過去資料以找出差異"
-#: taiga/users/admin.py:51
+#: taiga/users/admin.py:38
+msgid "Project Member"
+msgstr ""
+
+#: taiga/users/admin.py:39
+msgid "Project Members"
+msgstr ""
+
+#: taiga/users/admin.py:49
+msgid "id"
+msgstr ""
+
+#: taiga/users/admin.py:81
+msgid "Project Ownership"
+msgstr ""
+
+#: taiga/users/admin.py:82
+msgid "Project Ownerships"
+msgstr ""
+
+#: taiga/users/admin.py:119
msgid "Personal info"
msgstr "個人資訊"
-#: taiga/users/admin.py:54
+#: taiga/users/admin.py:122
msgid "Permissions"
msgstr "許可"
-#: taiga/users/admin.py:55
+#: taiga/users/admin.py:123
msgid "Restrictions"
msgstr ""
-#: taiga/users/admin.py:57
+#: taiga/users/admin.py:125
msgid "Important dates"
msgstr "重要日期"
From 00852e22987a486d713216be63983b7a8e41282a Mon Sep 17 00:00:00 2001
From: everblut
Date: Fri, 12 Feb 2016 02:30:35 -0600
Subject: [PATCH 11/19] US #3903: Sprint number/title column on search results
for user stories
---
AUTHORS.rst | 1 +
CHANGELOG.md | 1 +
taiga/searches/serializers.py | 3 ++-
3 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index ec62d162..5be3cfd6 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -27,6 +27,7 @@ answer newbie questions, and generally made taiga that much better:
- Bruno Clermont
- Chris Wilson
- David Burke
+- Everardo Medina
- Hector Colina
- Joe Letts
- Julien Palard
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cc6c0a4..a2fb6116 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
- ...
### Misc
+- Add sprint name and slug on search results for user stories ((thanks to [@everblut](https://github.com/everblut)))
- Lots of small and not so small bugfixes.
diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py
index d1173b65..1938b5d3 100644
--- a/taiga/searches/serializers.py
+++ b/taiga/searches/serializers.py
@@ -41,7 +41,8 @@ class TaskSearchResultsSerializer(TaskSerializer):
class UserStorySearchResultsSerializer(UserStorySerializer):
class Meta:
model = UserStory
- fields = ('id', 'ref', 'subject', 'status', 'total_points')
+ fields = ('id', 'ref', 'subject', 'status', 'total_points',
+ 'milestone_name', 'milestone_slug')
class WikiPageSearchResultsSerializer(WikiPageSerializer):
From 9f4886963a00b38bf0d0e30eb43a4cecbe333207 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 11 Apr 2016 21:30:00 +0200
Subject: [PATCH 12/19] Improve webhooks (fix #3688, #3755 and #3758)
---
CHANGELOG.md | 9 +-
taiga/base/utils/json.py | 4 +-
taiga/webhooks/api.py | 3 +-
taiga/webhooks/serializers.py | 363 +++++++++++++++---
taiga/webhooks/signal_handlers.py | 7 +-
taiga/webhooks/tasks.py | 92 +++--
tests/integration/test_webhooks_issues.py | 248 ++++++++++++
tests/integration/test_webhooks_milestones.py | 101 +++++
...t_webhooks.py => test_webhooks_signals.py} | 68 ++--
tests/integration/test_webhooks_tasks.py | 248 ++++++++++++
.../integration/test_webhooks_userstories.py | 308 +++++++++++++++
tests/integration/test_webhooks_wikipages.py | 170 ++++++++
12 files changed, 1489 insertions(+), 132 deletions(-)
create mode 100644 tests/integration/test_webhooks_issues.py
create mode 100644 tests/integration/test_webhooks_milestones.py
rename tests/integration/{test_webhooks.py => test_webhooks_signals.py} (59%)
create mode 100644 tests/integration/test_webhooks_tasks.py
create mode 100644 tests/integration/test_webhooks_userstories.py
create mode 100644 tests/integration/test_webhooks_wikipages.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2fb6116..c18e289e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,14 @@
## 2.1.0 ??? (unreleased)
### Features
-- ...
+- Webhooks: Improve webhook data:
+ - add permalinks
+ - owner, assigned_to, status, type, priority, severity, user_story, milestone, project are objects
+ - add role to 'points' object
+ - add the owner to every notification ('by' field)
+ - add the date of the notification ('date' field)
+ - show human diffs in 'changes'
+ - remove unnecessary data
### Misc
- Add sprint name and slug on search results for user stories ((thanks to [@everblut](https://github.com/everblut)))
diff --git a/taiga/base/utils/json.py b/taiga/base/utils/json.py
index 5cb8f6b6..a5e0a477 100644
--- a/taiga/base/utils/json.py
+++ b/taiga/base/utils/json.py
@@ -22,8 +22,8 @@ from taiga.base.api.utils import encoders
import json
-def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder):
- return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii)
+def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder, indent=None):
+ return json.dumps(data, cls=encoder_class, ensure_ascii=ensure_ascii, indent=indent)
def loads(data):
diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py
index a9b8545e..426537dc 100644
--- a/taiga/webhooks/api.py
+++ b/taiga/webhooks/api.py
@@ -15,6 +15,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.utils import timezone
from django.utils.translation import ugettext as _
from taiga.base import filters
@@ -45,7 +46,7 @@ class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
self.check_permissions(request, 'test', webhook)
self.pre_conditions_blocked(webhook)
- webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key)
+ webhooklog = tasks.test_webhook(webhook.id, webhook.url, webhook.key, request.user, timezone.now())
log = serializers.WebhookLogSerializer(webhooklog)
return response.Ok(log.data)
diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py
index a2714a05..a2553b69 100644
--- a/taiga/webhooks/serializers.py
+++ b/taiga/webhooks/serializers.py
@@ -20,21 +20,26 @@ from django.core.exceptions import ObjectDoesNotExist
from taiga.base.api import serializers
from taiga.base.fields import TagsField, PgArrayField, JsonField
-from taiga.projects.userstories import models as us_models
-from taiga.projects.tasks import models as task_models
+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.wiki import models as wiki_models
-from taiga.projects.history import models as history_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.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
-class HistoryDiffField(serializers.Field):
- def to_native(self, obj):
- return {key: {"from": value[0], "to": value[1]} for key, value in obj.items()}
-
+########################################################################
+## WebHooks
+########################################################################
class WebhookSerializer(serializers.ModelSerializer):
logs_counter = serializers.SerializerMethodField("get_logs_counter")
@@ -55,16 +60,93 @@ class WebhookLogSerializer(serializers.ModelSerializer):
model = WebhookLog
+########################################################################
+## User
+########################################################################
+
class UserSerializer(serializers.Serializer):
id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
+ 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
- def get_name(self, obj):
- return obj.full_name
+ 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
+
+ def get_full_name(self, obj):
+ return obj.get_full_name()
+
+ def get_photo(self, obj):
+ return get_photo_or_gravatar_url(obj)
+
+########################################################################
+## 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
+
+ def get_permalink(self, obj):
+ return resolve_front_url("project", obj.slug)
+
+ def get_name(self, obj):
+ return obj.name
+
+ def get_logo_big_url(self, obj):
+ return get_logo_big_thumbnail_url(obj)
+
+
+########################################################################
+## History Serializer
+########################################################################
+
+class HistoryDiffField(serializers.Field):
+ def to_native(self, value):
+ # Tip: 'value' is the object returned by
+ # taiga.projects.history.models.HistoryEntry.values_diff()
+
+ ret = {}
+
+ for key, val in value.items():
+ if key in ["attachments", "custom_attributes"]:
+ ret[key] = val
+ elif key == "points":
+ ret[key] = {k: {"from": v[0], "to": v[1]} for k, v in val.items()}
+ else:
+ ret[key] = {"from": val[0], "to": val[1]}
+
+ 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")
+
+
+########################################################################
+## _Misc_
+########################################################################
class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
@@ -90,86 +172,251 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
except ObjectDoesNotExist:
return None
-class PointSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
+
+class RolePointsSerializer(serializers.Serializer):
+ role = serializers.SerializerMethodField("get_role")
name = serializers.SerializerMethodField("get_name")
value = serializers.SerializerMethodField("get_value")
+ def get_role(self, obj):
+ return obj.role.name
+
+ def get_name(self, obj):
+ return obj.points.name
+
+ def get_value(self, obj):
+ 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
def get_name(self, obj):
return obj.name
- def get_value(self, obj):
- return obj.value
+ 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
+
+ def get_is_archived(self, obj):
+ return obj.is_archived
-class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- tags = TagsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
- owner = UserSerializer()
- assigned_to = UserSerializer()
- points = PointSerializer(many=True)
+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")
- class Meta:
- model = us_models.UserStory
- exclude = ("backlog_order", "sprint_order", "kanban_order", "version")
+ def get_pk(self, obj):
+ return obj.pk
- def custom_attributes_queryset(self, project):
- return project.userstorycustomattributes.all()
+ 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 TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- tags = TagsField(default=[], required=False)
- owner = UserSerializer()
- assigned_to = UserSerializer()
+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")
- class Meta:
- model = task_models.Task
+ def get_pk(self, obj):
+ return obj.pk
- def custom_attributes_queryset(self, project):
- return project.taskcustomattributes.all()
+ 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 IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- tags = TagsField(default=[], required=False)
- owner = UserSerializer()
- assigned_to = UserSerializer()
+class IssueTypeSerializer(serializers.Serializer):
+ id = serializers.SerializerMethodField("get_pk")
+ name = serializers.SerializerMethodField("get_name")
+ color = serializers.SerializerMethodField("get_color")
- class Meta:
- model = issue_models.Issue
+ def get_pk(self, obj):
+ return obj.pk
- def custom_attributes_queryset(self, project):
- return project.issuecustomattributes.all()
+ def get_name(self, obj):
+ return obj.name
+
+ def get_color(self, obj):
+ return obj.color
-class WikiPageSerializer(serializers.ModelSerializer):
- owner = UserSerializer()
- last_modifier = UserSerializer()
+class PrioritySerializer(serializers.Serializer):
+ id = serializers.SerializerMethodField("get_pk")
+ name = serializers.SerializerMethodField("get_name")
+ color = serializers.SerializerMethodField("get_color")
- class Meta:
- model = wiki_models.WikiPage
- exclude = ("watchers", "version")
+ def get_pk(self, obj):
+ return obj.pk
+ def get_name(self, obj):
+ return obj.name
+
+ def get_color(self, obj):
+ 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
+
+ def get_name(self, obj):
+ return obj.name
+
+ def get_color(self, obj):
+ return obj.color
+
+
+########################################################################
+## Milestone
+########################################################################
class MilestoneSerializer(serializers.ModelSerializer):
+ 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)
-class HistoryEntrySerializer(serializers.ModelSerializer):
- diff = HistoryDiffField()
- snapshot = JsonField()
- values = JsonField()
- user = JsonField()
- delete_comment_user = JsonField()
+
+########################################################################
+## User Story
+########################################################################
+
+class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
+ serializers.ModelSerializer):
+ permalink = serializers.SerializerMethodField("get_permalink")
+ tags = TagsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+ project = ProjectSerializer()
+ owner = UserSerializer()
+ assigned_to = UserSerializer()
+ points = RolePointsSerializer(source="role_points", many=True)
+ status = UserStoryStatusSerializer()
+ milestone = MilestoneSerializer()
class Meta:
- model = history_models.HistoryEntry
+ 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()
+
+
+########################################################################
+## Task
+########################################################################
+
+class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
+ serializers.ModelSerializer):
+ permalink = serializers.SerializerMethodField("get_permalink")
+ tags = TagsField(default=[], required=False)
+ project = ProjectSerializer()
+ owner = UserSerializer()
+ assigned_to = UserSerializer()
+ status = TaskStatusSerializer()
+ 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()
+
+
+########################################################################
+## Issue
+########################################################################
+
+class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
+ serializers.ModelSerializer):
+ permalink = serializers.SerializerMethodField("get_permalink")
+ tags = TagsField(default=[], required=False)
+ project = ProjectSerializer()
+ milestone = MilestoneSerializer()
+ owner = UserSerializer()
+ assigned_to = UserSerializer()
+ status = IssueStatusSerializer()
+ type = IssueTypeSerializer()
+ 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()
+
+
+########################################################################
+## Wiki Page
+########################################################################
+
+class WikiPageSerializer(serializers.ModelSerializer):
+ 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/signal_handlers.py b/taiga/webhooks/signal_handlers.py
index 42755553..415d4fcf 100644
--- a/taiga/webhooks/signal_handlers.py
+++ b/taiga/webhooks/signal_handlers.py
@@ -61,10 +61,13 @@ def on_new_history_entry(sender, instance, created, **kwargs):
extra_args = [instance]
elif instance.type == HistoryType.delete:
task = tasks.delete_webhook
- extra_args = [timezone.now()]
+ extra_args = []
+
+ by = instance.owner
+ date = timezone.now()
for webhook in webhooks:
- args = [webhook["id"], webhook["url"], webhook["key"], obj] + extra_args
+ args = [webhook["id"], webhook["url"], webhook["key"], by, date, obj] + extra_args
if settings.CELERY_ENABLED:
connection.on_commit(lambda: task.delay(*args))
diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py
index c9ca6b88..7427fe6e 100644
--- a/taiga/webhooks/tasks.py
+++ b/taiga/webhooks/tasks.py
@@ -26,7 +26,7 @@ from taiga.celery import app
from .serializers import (UserStorySerializer, IssueSerializer, TaskSerializer,
WikiPageSerializer, MilestoneSerializer,
- HistoryEntrySerializer)
+ HistoryEntrySerializer, UserSerializer)
from .models import WebhookLog
@@ -67,58 +67,70 @@ def _send_request(webhook_id, url, key, data):
request = requests.Request('POST', url, data=serialized_data, headers=headers)
prepared_request = request.prepare()
- session = requests.Session()
- try:
- response = session.send(prepared_request)
- webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url,
- status=response.status_code,
- request_data=data,
- request_headers=dict(prepared_request.headers),
- response_data=response.content,
- response_headers=dict(response.headers),
- duration=response.elapsed.total_seconds())
- except RequestException as e:
- webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url, status=0,
- request_data=data,
- request_headers=dict(prepared_request.headers),
- response_data="error-in-request: {}".format(str(e)),
- response_headers={},
- duration=0)
- session.close()
+ with requests.Session() as session:
+ try:
+ response = session.send(prepared_request)
+ except RequestException as e:
+ # Error sending the webhook
+ webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url, status=0,
+ request_data=data,
+ request_headers=dict(prepared_request.headers),
+ response_data="error-in-request: {}".format(str(e)),
+ response_headers={},
+ duration=0)
+ else:
+ # Webhook was sent successfully
+ webhook_log = WebhookLog.objects.create(webhook_id=webhook_id, url=url,
+ status=response.status_code,
+ request_data=data,
+ request_headers=dict(prepared_request.headers),
+ response_data=response.content,
+ response_headers=dict(response.headers),
+ duration=response.elapsed.total_seconds())
+ finally:
+ # Only the last ten webhook logs traces are required
+ # so remove the leftover
+ ids = (WebhookLog.objects.filter(webhook_id=webhook_id)
+ .order_by("-id")
+ .values_list('id', flat=True)[10:])
+ WebhookLog.objects.filter(id__in=ids).delete()
- ids = [log.id for log in WebhookLog.objects.filter(webhook_id=webhook_id).order_by("-id")[10:]]
- WebhookLog.objects.filter(id__in=ids).delete()
return webhook_log
@app.task
-def change_webhook(webhook_id, url, key, obj, change):
+def create_webhook(webhook_id, url, key, by, date, obj):
data = {}
- data['data'] = _serialize(obj)
- data['action'] = "change"
- data['type'] = _get_type(obj)
- data['change'] = _serialize(change)
-
- return _send_request(webhook_id, url, key, data)
-
-
-@app.task
-def create_webhook(webhook_id, url, key, obj):
- data = {}
- data['data'] = _serialize(obj)
data['action'] = "create"
data['type'] = _get_type(obj)
+ data['by'] = UserSerializer(by).data
+ data['date'] = date
+ data['data'] = _serialize(obj)
return _send_request(webhook_id, url, key, data)
@app.task
-def delete_webhook(webhook_id, url, key, obj, deleted_date):
+def delete_webhook(webhook_id, url, key, by, date, obj):
data = {}
- data['data'] = _serialize(obj)
data['action'] = "delete"
data['type'] = _get_type(obj)
- data['deleted_date'] = deleted_date
+ data['by'] = UserSerializer(by).data
+ data['date'] = date
+ data['data'] = _serialize(obj)
+
+ return _send_request(webhook_id, url, key, data)
+
+
+@app.task
+def change_webhook(webhook_id, url, key, by, date, obj, change):
+ data = {}
+ data['action'] = "change"
+ data['type'] = _get_type(obj)
+ data['by'] = UserSerializer(by).data
+ data['date'] = date
+ data['data'] = _serialize(obj)
+ data['change'] = _serialize(change)
return _send_request(webhook_id, url, key, data)
@@ -129,10 +141,12 @@ def resend_webhook(webhook_id, url, key, data):
@app.task
-def test_webhook(webhook_id, url, key):
+def test_webhook(webhook_id, url, key, by, date):
data = {}
- data['data'] = {"test": "test"}
data['action'] = "test"
data['type'] = "test"
+ data['by'] = UserSerializer(by).data
+ data['date'] = date
+ data['data'] = {"test": "test"}
return _send_request(webhook_id, url, key, data)
diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py
new file mode 100644
index 00000000..95f6a4a2
--- /dev/null
+++ b/tests/integration/test_webhooks_issues.py
@@ -0,0 +1,248 @@
+# 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 unittest.mock import Mock
+
+from .. import factories as f
+
+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()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.IssueFactory.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"] == "issue"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+
+
+def test_webhooks_when_update_issue(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.IssueFactory.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"] == "issue"
+ 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_issue(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.IssueFactory.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"] == "issue"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert "data" in data
+
+
+def test_webhooks_when_update_issue_attachments(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.IssueFactory.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.IssueAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner)
+ attachment2 = f.IssueAttachmentFactory(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"] == "issue"
+ 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"] == "issue"
+ 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"] == "issue"
+ 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_issue_custom_attributes(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.IssueFactory.create(project=project)
+
+ custom_attr_1 = f.IssueCustomAttributeFactory(project=obj.project)
+ ct1_id = "{}".format(custom_attr_1.id)
+ custom_attr_2 = f.IssueCustomAttributeFactory(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"] == "issue"
+ 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"] == "issue"
+ 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"] == "issue"
+ 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
diff --git a/tests/integration/test_webhooks_milestones.py b/tests/integration/test_webhooks_milestones.py
new file mode 100644
index 00000000..f720df2f
--- /dev/null
+++ b/tests/integration/test_webhooks_milestones.py
@@ -0,0 +1,101 @@
+# 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 unittest.mock import Mock
+
+from .. import factories as f
+
+from taiga.projects.history import services
+
+
+pytestmark = pytest.mark.django_db(transaction=True)
+
+
+from taiga.base.utils import json
+
+def test_webhooks_when_create_milestone(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.MilestoneFactory.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"] == "milestone"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+
+
+def test_webhooks_when_update_milestone(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.MilestoneFactory.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.name = "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"] == "milestone"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+ assert data["data"]["name"] == obj.name
+ assert data["change"]["comment"] == "test_comment"
+ assert data["change"]["diff"]["name"]["to"] == data["data"]["name"]
+ assert data["change"]["diff"]["name"]["from"] != data["data"]["name"]
+
+
+def test_webhooks_when_delete_milestone(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.MilestoneFactory.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"] == "milestone"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert "data" in data
diff --git a/tests/integration/test_webhooks.py b/tests/integration/test_webhooks_signals.py
similarity index 59%
rename from tests/integration/test_webhooks.py
rename to tests/integration/test_webhooks_signals.py
index 83ee4b28..cf9996ca 100644
--- a/tests/integration/test_webhooks.py
+++ b/tests/integration/test_webhooks_signals.py
@@ -18,6 +18,7 @@
import pytest
from unittest.mock import patch
+from unittest.mock import Mock
from .. import factories as f
@@ -26,7 +27,7 @@ from taiga.projects.history import services
pytestmark = pytest.mark.django_db(transaction=True)
-def test_new_object_with_one_webhook(settings):
+def test_new_object_with_one_webhook_signal(settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
f.WebhookFactory.create(project=project)
@@ -38,28 +39,31 @@ def test_new_object_with_one_webhook(settings):
f.WikiPageFactory.create(project=project)
]
- for obj in objects:
- with patch('taiga.webhooks.tasks.create_webhook') as create_webhook_mock:
- services.take_snapshot(obj, user=obj.owner, comment="test")
- assert create_webhook_mock.call_count == 1
+ response = Mock(status_code=200, headers={}, content="ok")
+ response.elapsed.total_seconds.return_value = 100
for obj in objects:
- with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test")
+ assert session_send_mock.call_count == 1
+
+ for obj in objects:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner)
- assert change_webhook_mock.call_count == 0
+ assert session_send_mock.call_count == 0
for obj in objects:
- with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner, comment="test")
- assert change_webhook_mock.call_count == 1
+ assert session_send_mock.call_count == 1
for obj in objects:
- with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
- assert delete_webhook_mock.call_count == 1
+ assert session_send_mock.call_count == 1
-def test_new_object_with_two_webhook(settings):
+def test_new_object_with_two_webhook_signals(settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
f.WebhookFactory.create(project=project)
@@ -72,28 +76,31 @@ def test_new_object_with_two_webhook(settings):
f.WikiPageFactory.create(project=project)
]
- for obj in objects:
- with patch('taiga.webhooks.tasks.create_webhook') as create_webhook_mock:
- services.take_snapshot(obj, user=obj.owner, comment="test")
- assert create_webhook_mock.call_count == 2
+ response = Mock(status_code=200, headers={}, content="ok")
+ response.elapsed.total_seconds.return_value = 100
for obj in objects:
- with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner, comment="test")
- assert change_webhook_mock.call_count == 2
+ assert session_send_mock.call_count == 2
for obj in objects:
- with patch('taiga.webhooks.tasks.change_webhook') as change_webhook_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test")
+ assert session_send_mock.call_count == 2
+
+ for obj in objects:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner)
- assert change_webhook_mock.call_count == 0
+ assert session_send_mock.call_count == 0
for obj in objects:
- with patch('taiga.webhooks.tasks.delete_webhook') as delete_webhook_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
- assert delete_webhook_mock.call_count == 2
+ assert session_send_mock.call_count == 2
-def test_send_request_one_webhook(settings):
+def test_send_request_one_webhook_signal(settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
f.WebhookFactory.create(project=project)
@@ -105,12 +112,15 @@ def test_send_request_one_webhook(settings):
f.WikiPageFactory.create(project=project)
]
- for obj in objects:
- with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
- services.take_snapshot(obj, user=obj.owner, comment="test")
- assert _send_request_mock.call_count == 1
+ response = Mock(status_code=200, headers={}, content="ok")
+ response.elapsed.total_seconds.return_value = 100
for obj in objects:
- with patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
+ services.take_snapshot(obj, user=obj.owner, comment="test")
+ assert session_send_mock.call_count == 1
+
+ for obj in objects:
+ with patch("taiga.webhooks.tasks.requests.Session.send", return_value=response) as session_send_mock:
services.take_snapshot(obj, user=obj.owner, comment="test", delete=True)
- assert _send_request_mock.call_count == 1
+ assert session_send_mock.call_count == 1
diff --git a/tests/integration/test_webhooks_tasks.py b/tests/integration/test_webhooks_tasks.py
new file mode 100644
index 00000000..ba5eda8d
--- /dev/null
+++ b/tests/integration/test_webhooks_tasks.py
@@ -0,0 +1,248 @@
+# 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 unittest.mock import Mock
+
+from .. import factories as f
+
+from taiga.projects.history import services
+
+
+pytestmark = pytest.mark.django_db(transaction=True)
+
+
+from taiga.base.utils import json
+
+def test_webhooks_when_create_task(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.TaskFactory.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"] == "task"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+
+
+def test_webhooks_when_update_task(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.TaskFactory.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"] == "task"
+ 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_task(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.TaskFactory.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"] == "task"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert "data" in data
+
+
+def test_webhooks_when_update_task_attachments(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.TaskFactory.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.TaskAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner)
+ attachment2 = f.TaskAttachmentFactory(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"] == "task"
+ 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"] == "task"
+ 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"] == "task"
+ 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_task_custom_attributes(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.TaskFactory.create(project=project)
+
+ custom_attr_1 = f.TaskCustomAttributeFactory(project=obj.project)
+ ct1_id = "{}".format(custom_attr_1.id)
+ custom_attr_2 = f.TaskCustomAttributeFactory(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"] == "task"
+ 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"] == "task"
+ 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"] == "task"
+ 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
diff --git a/tests/integration/test_webhooks_userstories.py b/tests/integration/test_webhooks_userstories.py
new file mode 100644
index 00000000..716697ce
--- /dev/null
+++ b/tests/integration/test_webhooks_userstories.py
@@ -0,0 +1,308 @@
+# 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 unittest.mock import Mock
+
+from .. import factories as f
+
+from taiga.projects.history import services
+
+
+pytestmark = pytest.mark.django_db(transaction=True)
+
+
+from taiga.base.utils import json
+
+def test_webhooks_when_create_user_story(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.UserStoryFactory.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"] == "userstory"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+
+
+def test_webhooks_when_update_user_story(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.UserStoryFactory.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"] == "userstory"
+ 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_user_story(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.UserStoryFactory.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"] == "userstory"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert "data" in data
+
+
+def test_webhooks_when_update_user_story_attachments(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.UserStoryFactory.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.UserStoryAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner)
+ attachment2 = f.UserStoryAttachmentFactory(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"] == "userstory"
+ 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"] == "userstory"
+ 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"] == "userstory"
+ 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_user_story_custom_attributes(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.UserStoryFactory.create(project=project)
+
+ custom_attr_1 = f.UserStoryCustomAttributeFactory(project=obj.project)
+ ct1_id = "{}".format(custom_attr_1.id)
+ custom_attr_2 = f.UserStoryCustomAttributeFactory(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"] == "userstory"
+ 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"] == "userstory"
+ 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"] == "userstory"
+ 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_update_user_story_points(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ role1 = f.RoleFactory.create(project=project)
+ role2 = f.RoleFactory.create(project=project)
+
+ points1 = f.PointsFactory.create(project=project, value=None)
+ points2 = f.PointsFactory.create(project=project, value=1)
+ points3 = f.PointsFactory.create(project=project, value=2)
+
+ obj = f.UserStoryFactory.create(project=project)
+ obj.role_points.all().delete()
+
+ 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
+
+ # Set points
+ f.RolePointsFactory.create(user_story=obj, role=role1, points=points1)
+ f.RolePointsFactory.create(user_story=obj, role=role2, points=points2)
+
+ 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"] == "change"
+ assert data["type"] == "userstory"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+ assert data["change"]["comment"] == ""
+ assert data["change"]["diff"]["points"][role1.name]["from"] == None
+ assert data["change"]["diff"]["points"][role1.name]["to"] == points1.name
+ assert data["change"]["diff"]["points"][role2.name]["from"] == None
+ assert data["change"]["diff"]["points"][role2.name]["to"] == points2.name
+
+ # Change points
+ obj.role_points.all().update(points=points3)
+
+ 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"] == "change"
+ assert data["type"] == "userstory"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+ assert data["change"]["comment"] == ""
+ assert data["change"]["diff"]["points"][role1.name]["from"] == points1.name
+ assert data["change"]["diff"]["points"][role1.name]["to"] == points3.name
+ assert data["change"]["diff"]["points"][role2.name]["from"] == points2.name
+ assert data["change"]["diff"]["points"][role2.name]["to"] == points3.name
diff --git a/tests/integration/test_webhooks_wikipages.py b/tests/integration/test_webhooks_wikipages.py
new file mode 100644
index 00000000..5d10f233
--- /dev/null
+++ b/tests/integration/test_webhooks_wikipages.py
@@ -0,0 +1,170 @@
+# 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 unittest.mock import Mock
+
+from .. import factories as f
+
+from taiga.projects.history import services
+
+
+pytestmark = pytest.mark.django_db(transaction=True)
+
+
+from taiga.base.utils import json
+
+def test_webhooks_when_create_wiki_page(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.WikiPageFactory.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"] == "wikipage"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+
+
+def test_webhooks_when_update_wiki_page(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.WikiPageFactory.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.content = "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"] == "wikipage"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert data["data"]["id"] == obj.id
+ assert data["data"]["content"] == obj.content
+ assert data["change"]["comment"] == "test_comment"
+ assert data["change"]["diff"]["content_html"]["from"] != data["change"]["diff"]["content_html"]["to"]
+ assert obj.content in data["change"]["diff"]["content_html"]["to"]
+
+
+def test_webhooks_when_delete_wiki_page(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.WikiPageFactory.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"] == "wikipage"
+ assert data["by"]["id"] == obj.owner.id
+ assert "date" in data
+ assert "data" in data
+
+
+def test_webhooks_when_update_wiki_page_attachments(settings):
+ settings.WEBHOOKS_ENABLED = True
+ project = f.ProjectFactory()
+ f.WebhookFactory.create(project=project)
+ f.WebhookFactory.create(project=project)
+
+ obj = f.WikiPageFactory.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.WikiAttachmentFactory(project=obj.project, content_object=obj, owner=obj.owner)
+ attachment2 = f.WikiAttachmentFactory(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"] == "wikipage"
+ 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"] == "wikipage"
+ 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"] == "wikipage"
+ 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
From 0877a5af28001881f985bf4525b0e041d477d0ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 14 Apr 2016 12:57:19 +0200
Subject: [PATCH 13/19] Fix issue #4094: Add 'estimated start' and 'estimated
end' sprint dates to CSV reports
---
CHANGELOG.md | 3 +++
taiga/projects/issues/services.py | 15 ++++++++-------
taiga/projects/tasks/services.py | 13 +++++++------
taiga/projects/userstories/services.py | 13 +++++++------
tests/integration/test_issues.py | 4 ++--
tests/integration/test_tasks.py | 6 +++---
tests/integration/test_userstories.py | 4 ++--
7 files changed, 32 insertions(+), 26 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c18e289e..8a16fe64 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@
- add the date of the notification ('date' field)
- show human diffs in 'changes'
- remove unnecessary data
+- CSV Reports:
+ - Change field name: 'milestone' to 'sprint'
+ - Add new fields: 'sprint_estimated_start' and 'sprint_estimated_end'
### Misc
- Add sprint name and slug on search results for user stories ((thanks to [@everblut](https://github.com/everblut)))
diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py
index 6da95c9b..6a6c5a37 100644
--- a/taiga/projects/issues/services.py
+++ b/taiga/projects/issues/services.py
@@ -84,12 +84,11 @@ def update_issues_order_in_bulk(bulk_data):
def issues_to_csv(project, queryset):
csv_data = io.StringIO()
- fieldnames = ["ref", "subject", "description", "milestone", "owner",
- "owner_full_name", "assigned_to", "assigned_to_full_name",
- "status", "severity", "priority", "type", "is_closed",
- "attachments", "external_reference", "tags",
- "watchers", "voters",
- "created_date", "modified_date", "finished_date"]
+ fieldnames = ["ref", "subject", "description", "sprint", "sprint_estimated_start",
+ "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to",
+ "assigned_to_full_name", "status", "severity", "priority", "type",
+ "is_closed", "attachments", "external_reference", "tags", "watchers",
+ "voters", "created_date", "modified_date", "finished_date"]
custom_attrs = project.issuecustomattributes.all()
for custom_attr in custom_attrs:
@@ -112,7 +111,9 @@ def issues_to_csv(project, queryset):
"ref": issue.ref,
"subject": issue.subject,
"description": issue.description,
- "milestone": issue.milestone.name if issue.milestone else None,
+ "sprint": issue.milestone.name if issue.milestone else None,
+ "sprint_estimated_start": issue.milestone.estimated_start if issue.milestone else None,
+ "sprint_estimated_finish": issue.milestone.estimated_finish if issue.milestone else None,
"owner": issue.owner.username if issue.owner else None,
"owner_full_name": issue.owner.get_full_name() if issue.owner else None,
"assigned_to": issue.assigned_to.username if issue.assigned_to else None,
diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py
index 7e3e97ce..e1f76f67 100644
--- a/taiga/projects/tasks/services.py
+++ b/taiga/projects/tasks/services.py
@@ -95,11 +95,10 @@ def snapshot_tasks_in_bulk(bulk_data, user):
def tasks_to_csv(project, queryset):
csv_data = io.StringIO()
- fieldnames = ["ref", "subject", "description", "user_story", "milestone", "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"]
+ 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"]
custom_attrs = project.taskcustomattributes.all()
for custom_attr in custom_attrs:
@@ -124,7 +123,9 @@ def tasks_to_csv(project, queryset):
"subject": task.subject,
"description": task.description,
"user_story": task.user_story.ref if task.user_story else None,
- "milestone": task.milestone.name if task.milestone else None,
+ "sprint": task.milestone.name if task.milestone else None,
+ "sprint_estimated_start": task.milestone.estimated_start if task.milestone else None,
+ "sprint_estimated_finish": task.milestone.estimated_finish if task.milestone else None,
"owner": task.owner.username if task.owner else None,
"owner_full_name": task.owner.get_full_name() if task.owner else None,
"assigned_to": task.assigned_to.username if task.assigned_to else None,
diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py
index b8a0533c..b0b881a6 100644
--- a/taiga/projects/userstories/services.py
+++ b/taiga/projects/userstories/services.py
@@ -130,9 +130,9 @@ def open_userstory(us):
def userstories_to_csv(project,queryset):
csv_data = io.StringIO()
- fieldnames = ["ref", "subject", "description", "milestone", "owner",
- "owner_full_name", "assigned_to", "assigned_to_full_name",
- "status", "is_closed"]
+ fieldnames = ["ref", "subject", "description", "sprint", "sprint_estimated_start",
+ "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to",
+ "assigned_to_full_name", "status", "is_closed"]
roles = project.roles.filter(computable=True).order_by('slug')
for role in roles:
@@ -144,8 +144,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:
@@ -174,7 +173,9 @@ def userstories_to_csv(project,queryset):
"ref": us.ref,
"subject": us.subject,
"description": us.description,
- "milestone": us.milestone.name if us.milestone else None,
+ "sprint": us.milestone.name if us.milestone else None,
+ "sprint_estimated_start": us.milestone.estimated_start if us.milestone else None,
+ "sprint_estimated_finish": us.milestone.estimated_finish if us.milestone else None,
"owner": us.owner.username if us.owner else None,
"owner_full_name": us.owner.get_full_name() if us.owner else None,
"assigned_to": us.assigned_to.username if us.assigned_to else None,
diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py
index 71f45f3f..14b01c60 100644
--- a/tests/integration/test_issues.py
+++ b/tests/integration/test_issues.py
@@ -433,6 +433,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[21] == attr.name
+ assert row[23] == attr.name
row = next(reader)
- assert row[21] == "val1"
+ assert row[23] == "val1"
diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py
index 2e8203fd..ee897b5f 100644
--- a/tests/integration/test_tasks.py
+++ b/tests/integration/test_tasks.py
@@ -64,7 +64,7 @@ def test_create_task_without_default_values(client):
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
assert response.data['status'] == None
-
+
def test_api_update_task_tags(client):
project = f.ProjectFactory.create()
@@ -176,6 +176,6 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[19] == attr.name
+ assert row[21] == attr.name
row = next(reader)
- assert row[19] == "val1"
+ assert row[21] == "val1"
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index 75b3098a..8081eefd 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -469,9 +469,9 @@ def test_custom_fields_csv_generation():
data.seek(0)
reader = csv.reader(data)
row = next(reader)
- assert row[26] == attr.name
+ assert row[28] == attr.name
row = next(reader)
- assert row[26] == "val1"
+ assert row[28] == "val1"
def test_update_userstory_respecting_watchers(client):
From 19121220d62e65b5ae8ecf144557fbb2de6484a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 15 Apr 2016 14:10:34 +0200
Subject: [PATCH 14/19] Get a randomized list of featured projects if discover
mode is enabled
---
CHANGELOG.md | 4 +---
taiga/projects/filters.py | 4 ++++
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a16fe64..bc38a158 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,12 +15,10 @@
- CSV Reports:
- Change field name: 'milestone' to 'sprint'
- Add new fields: 'sprint_estimated_start' and 'sprint_estimated_end'
-
-### Misc
- 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`.
- Lots of small and not so small bugfixes.
-
## 2.0.0 Pulsatilla Patens (2016-04-04)
### Features
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index f01ed179..3bdb10c8 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -40,6 +40,10 @@ class DiscoverModeFilterBackend(FilterBackend):
qs = qs.filter(anon_permissions__contains=["view_project"],
blocked_code__isnull=True)
+ # random order for featured projects
+ if request.QUERY_PARAMS.get("is_featured", None) == 'true':
+ qs = qs.order_by("?")
+
return super().filter_queryset(request, qs.distinct(), view)
From e7748cb23635622dc7633efc4bd44a5ce862c284 Mon Sep 17 00:00:00 2001
From: Alejandro Alonso
Date: Wed, 20 Apr 2016 14:25:33 +0200
Subject: [PATCH 15/19] Adding is_out_of_owner_limits to project detail
serializer
---
taiga/projects/serializers.py | 4 +
taiga/projects/services/__init__.py | 1 +
taiga/projects/services/projects.py | 27 ++++
tests/integration/test_projects.py | 190 ++++++++++++++++++++++++++++
4 files changed, 222 insertions(+)
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 95f05703..661aeece 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -344,6 +344,7 @@ class ProjectDetailSerializer(ProjectSerializer):
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")
def get_members(self, obj):
qs = obj.memberships.filter(user__isnull=False)
@@ -356,6 +357,9 @@ class ProjectDetailSerializer(ProjectSerializer):
def get_total_memberships(self, obj):
return services.get_total_project_memberships(obj)
+ 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")
diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py
index d813423d..23d6334c 100644
--- a/taiga/projects/services/__init__.py
+++ b/taiga/projects/services/__init__.py
@@ -46,6 +46,7 @@ from .modules_config import get_modules_config
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 .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 306c585f..1ac6d2b7 100644
--- a/taiga/projects/services/projects.py
+++ b/taiga/projects/services/projects.py
@@ -124,3 +124,30 @@ def check_if_project_can_be_transfered(project, new_owner):
return (False, error_memberships_exceeded)
return (True, None)
+
+
+def check_if_project_is_out_of_owner_limits(project):
+ """Return if the project fits on its owner limits.
+
+ :param project: A project object.
+
+ :return: bool
+ """
+ if project.is_private:
+ current_memberships = project.memberships.count()
+ max_memberships = project.owner.max_memberships_private_projects
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ 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()
+ max_projects = project.owner.max_public_projects
+
+ if max_memberships is not None and current_memberships > max_memberships:
+ return True
+
+ if max_projects is not None and current_projects > max_projects:
+ return True
+
+ return False
diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py
index 14a8c7f4..6111747b 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -1624,3 +1624,193 @@ def test_public_project_can_be_private_because_project_has_unlimited_members(cli
project.owner.max_memberships_private_projects = None
assert (check_if_project_privacity_can_be_changed(project) == {'can_be_updated': True, 'reason': None})
+
+
+####################################################################################
+# Test taiga.projects.services.projects.check_if_project_is_out_of_owner_limit
+####################################################################################
+
+from taiga.projects.services.projects import check_if_project_is_out_of_owner_limits
+
+def test_private_project_when_owner_doesnt_have_enought_slot_and_too_much_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 0
+ project.owner.max_memberships_private_projects = 3
+
+ assert check_if_project_is_out_of_owner_limits(project) == True
+
+
+def test_private_project_when_owner_doesnt_have_enought_slot(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 0
+ project.owner.max_memberships_private_projects = 6
+
+ assert check_if_project_is_out_of_owner_limits(project) == True
+
+
+def test_private_project_when_too_much_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 2
+ project.owner.max_memberships_private_projects = 3
+
+ assert check_if_project_is_out_of_owner_limits(project) == True
+
+
+def test_private_project_when_owner_has_enought_slot_and_project_has_enought_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 2
+ project.owner.max_memberships_private_projects = 6
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_private_project_when_owner_has_unlimited_slot_and_project_has_unlimited_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = None
+ project.owner.max_memberships_private_projects = None
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_private_project_when_owner_has_unlimited_slot(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = None
+ project.owner.max_memberships_private_projects = 6
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_private_project_when_project_has_unlimited_members(client):
+ project = f.create_project(is_private=True)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_private_projects = 2
+ project.owner.max_memberships_private_projects = None
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+# public
+
+def test_public_project_when_owner_doesnt_have_enought_slot_and_too_much_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 0
+ project.owner.max_memberships_public_projects = 3
+
+ assert check_if_project_is_out_of_owner_limits(project) == True
+
+
+def test_public_project_when_owner_doesnt_have_enought_slot(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 0
+ project.owner.max_memberships_public_projects = 6
+
+ assert check_if_project_is_out_of_owner_limits(project) == True
+
+
+def test_public_project_when_too_much_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 2
+ project.owner.max_memberships_public_projects = 3
+
+ assert check_if_project_is_out_of_owner_limits(project) == True
+
+
+def test_public_project_when_owner_has_enought_slot_and_project_has_enought_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 2
+ project.owner.max_memberships_public_projects = 6
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_public_project_when_owner_has_unlimited_slot_and_project_has_unlimited_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = None
+ project.owner.max_memberships_public_projects = None
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_public_project_when_owner_has_unlimited_slot(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = None
+ project.owner.max_memberships_public_projects = 6
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
+
+
+def test_public_project_when_project_has_unlimited_members(client):
+ project = f.create_project(is_private=False)
+ f.MembershipFactory(project=project, user=project.owner)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+ f.MembershipFactory(project=project)
+
+ project.owner.max_public_projects = 2
+ project.owner.max_memberships_public_projects = None
+
+ assert check_if_project_is_out_of_owner_limits(project) == False
From 12b3998219f6964a207188f3ca36789dd54aa25e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Fri, 22 Apr 2016 16:48:39 +0200
Subject: [PATCH 16/19] Fix CHANGELOR
---
CHANGELOG.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc38a158..24145d95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,8 +17,11 @@
- Add new fields: 'sprint_estimated_start' and 'sprint_estimated_end'
- 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`.
+
+### Misc
- Lots of small and not so small bugfixes.
+
## 2.0.0 Pulsatilla Patens (2016-04-04)
### Features
From 283cf726b3cc985fedaf8350d77fff55adcfc1da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 21 Apr 2016 22:47:05 +0200
Subject: [PATCH 17/19] Fix issue #4114: Remove incomplete project if import
process fail
---
CHANGELOG.md | 7 +-
settings/local.py.example | 8 +
settings/testing.py | 4 +-
taiga/export_import/api.py | 170 +--
taiga/export_import/dump_service.py | 202 ---
taiga/export_import/exceptions.py | 23 +
.../management/commands/dump_project.py | 2 +-
.../management/commands/load_dump.py | 16 +-
taiga/export_import/services/__init__.py | 26 +
taiga/export_import/services/render.py | 124 ++
.../{service.py => services/store.py} | 736 +++++++----
taiga/export_import/tasks.py | 58 +-
tests/integration/test_importer_api.py | 1176 +++++++++--------
tests/unit/test_export.py | 2 +-
14 files changed, 1414 insertions(+), 1140 deletions(-)
delete mode 100644 taiga/export_import/dump_service.py
create mode 100644 taiga/export_import/exceptions.py
create mode 100644 taiga/export_import/services/__init__.py
create mode 100644 taiga/export_import/services/render.py
rename taiga/export_import/{service.py => services/store.py} (63%)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24145d95..7630f9ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
## 2.1.0 ??? (unreleased)
### Features
+- 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
- owner, assigned_to, status, type, priority, severity, user_story, milestone, project are objects
@@ -15,8 +17,9 @@
- CSV Reports:
- Change field name: 'milestone' to 'sprint'
- Add new fields: 'sprint_estimated_start' and 'sprint_estimated_end'
-- 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`.
+- Importer:
+ - Remove project after load a dump file fails
+ - Add more info the the logger if load a dump file fails
### Misc
- Lots of small and not so small bugfixes.
diff --git a/settings/local.py.example b/settings/local.py.example
index e1bd9383..09e53ca4 100644
--- a/settings/local.py.example
+++ b/settings/local.py.example
@@ -96,3 +96,11 @@ DATABASES = {
# 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
+#from .celery import *
+#CELERY_ENABLED = True
+#
+# To use celery in memory
+#CELERY_ENABLED = True
+#CELERY_ALWAYS_EAGER = True
diff --git a/settings/testing.py b/settings/testing.py
index b1549a8a..6862a5b1 100644
--- a/settings/testing.py
+++ b/settings/testing.py
@@ -17,10 +17,8 @@
from .development import *
-SKIP_SOUTH_TESTS = True
-SOUTH_TESTS_MIGRATE = False
-CELERY_ALWAYS_EAGER = True
CELERY_ENABLED = False
+CELERY_ALWAYS_EAGER = True
MEDIA_ROOT = "/tmp"
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index 5b570980..f84e263f 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -36,14 +36,14 @@ from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
from taiga.projects.serializers import ProjectSerializer
-from taiga.users import services as users_service
+from taiga.users import services as users_services
+from . import exceptions as err
from . import mixins
-from . import serializers
-from . import service
from . import permissions
+from . import serializers
+from . import services
from . import tasks
-from . import dump_service
from . import throttling
from .renderers import ExportRenderer
@@ -72,7 +72,7 @@ class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet)
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:
- service.render_project(project, outfile)
+ services.render_project(project, outfile)
response_data = {
"url": default_storage.url(path)
@@ -96,7 +96,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
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_service.has_available_slot_for_import_new_project(
+ (enough_slots, error_message) = users_services.has_available_slot_for_import_new_project(
self.request.user,
is_private,
total_memberships
@@ -105,22 +105,22 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
raise exc.NotEnoughSlotsForProject(is_private, total_memberships, error_message)
# Create Project
- project_serialized = service.store_project(data)
+ project_serialized = services.store.store_project(data)
if not project_serialized:
- raise exc.BadRequest(service.get_errors())
+ raise exc.BadRequest(services.store.get_errors())
# Create roles
roles_serialized = None
if "roles" in data:
- roles_serialized = service.store_roles(project_serialized.object, data)
+ roles_serialized = services.store.store_roles(project_serialized.object, data)
if not roles_serialized:
raise exc.BadRequest(_("We needed at least one role"))
# Create memberships
if "memberships" in data:
- service.store_memberships(project_serialized.object, data)
+ services.store.store_memberships(project_serialized.object, data)
try:
owner_membership = project_serialized.object.memberships.get(user=project_serialized.object.owner)
@@ -137,57 +137,57 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
# Create project values choicess
if "points" in data:
- service.store_choices(project_serialized.object, data,
- "points", serializers.PointsExportSerializer)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "points", serializers.PointsExportSerializer)
if "issue_types" in data:
- service.store_choices(project_serialized.object, data,
- "issue_types",
- serializers.IssueTypeExportSerializer)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "issue_types",
+ serializers.IssueTypeExportSerializer)
if "issue_statuses" in data:
- service.store_choices(project_serialized.object, data,
- "issue_statuses",
- serializers.IssueStatusExportSerializer,)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "issue_statuses",
+ serializers.IssueStatusExportSerializer,)
if "us_statuses" in data:
- service.store_choices(project_serialized.object, data,
- "us_statuses",
- serializers.UserStoryStatusExportSerializer,)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "us_statuses",
+ serializers.UserStoryStatusExportSerializer,)
if "task_statuses" in data:
- service.store_choices(project_serialized.object, data,
- "task_statuses",
- serializers.TaskStatusExportSerializer)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "task_statuses",
+ serializers.TaskStatusExportSerializer)
if "priorities" in data:
- service.store_choices(project_serialized.object, data,
- "priorities",
- serializers.PriorityExportSerializer)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "priorities",
+ serializers.PriorityExportSerializer)
if "severities" in data:
- service.store_choices(project_serialized.object, data,
- "severities",
- serializers.SeverityExportSerializer)
+ services.store.store_project_attributes_values(project_serialized.object, data,
+ "severities",
+ serializers.SeverityExportSerializer)
if ("points" in data or "issues_types" in data or
"issues_statuses" in data or "us_statuses" in data or
"task_statuses" in data or "priorities" in data or
"severities" in data):
- service.store_default_choices(project_serialized.object, data)
+ services.store.store_default_project_attributes_values(project_serialized.object, data)
# Created custom attributes
if "userstorycustomattributes" in data:
- service.store_custom_attributes(project_serialized.object, data,
- "userstorycustomattributes",
- serializers.UserStoryCustomAttributeExportSerializer)
+ services.store.store_custom_attributes(project_serialized.object, data,
+ "userstorycustomattributes",
+ serializers.UserStoryCustomAttributeExportSerializer)
if "taskcustomattributes" in data:
- service.store_custom_attributes(project_serialized.object, data,
- "taskcustomattributes",
- serializers.TaskCustomAttributeExportSerializer)
+ services.store.store_custom_attributes(project_serialized.object, data,
+ "taskcustomattributes",
+ serializers.TaskCustomAttributeExportSerializer)
if "issuecustomattributes" in data:
- service.store_custom_attributes(project_serialized.object, data,
- "issuecustomattributes",
- serializers.IssueCustomAttributeExportSerializer)
+ services.store.store_custom_attributes(project_serialized.object, data,
+ "issuecustomattributes",
+ serializers.IssueCustomAttributeExportSerializer)
# Is there any error?
- errors = service.get_errors()
+ errors = services.store.get_errors()
if errors:
raise exc.BadRequest(errors)
@@ -199,21 +199,33 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
@detail_route(methods=['post'])
@method_decorator(atomic)
- def issue(self, request, *args, **kwargs):
+ def milestone(self, request, *args, **kwargs):
project = self.get_object_or_none()
self.check_permissions(request, 'import_item', project)
- signals.pre_save.disconnect(sender=Issue,
- dispatch_uid="set_finished_date_when_edit_issue")
+ milestone = services.store.store_milestone(project, request.DATA.copy())
- issue = service.store_issue(project, request.DATA.copy())
-
- errors = service.get_errors()
+ errors = services.store.get_errors()
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(issue.data)
- return response.Created(issue.data, headers=headers)
+ headers = self.get_success_headers(milestone.data)
+ return response.Created(milestone.data, headers=headers)
+
+ @detail_route(methods=['post'])
+ @method_decorator(atomic)
+ def us(self, request, *args, **kwargs):
+ project = self.get_object_or_none()
+ self.check_permissions(request, 'import_item', project)
+
+ us = services.store.store_user_story(project, request.DATA.copy())
+
+ errors = services.store.get_errors()
+ if errors:
+ raise exc.BadRequest(errors)
+
+ headers = self.get_success_headers(us.data)
+ return response.Created(us.data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -224,9 +236,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
signals.pre_save.disconnect(sender=Task,
dispatch_uid="set_finished_date_when_edit_task")
- task = service.store_task(project, request.DATA.copy())
+ task = services.store.store_task(project, request.DATA.copy())
- errors = service.get_errors()
+ errors = services.store.get_errors()
if errors:
raise exc.BadRequest(errors)
@@ -235,33 +247,21 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
@detail_route(methods=['post'])
@method_decorator(atomic)
- def us(self, request, *args, **kwargs):
+ def issue(self, request, *args, **kwargs):
project = self.get_object_or_none()
self.check_permissions(request, 'import_item', project)
- us = service.store_user_story(project, request.DATA.copy())
+ signals.pre_save.disconnect(sender=Issue,
+ dispatch_uid="set_finished_date_when_edit_issue")
- errors = service.get_errors()
+ issue = services.store.store_issue(project, request.DATA.copy())
+
+ errors = services.store.get_errors()
if errors:
raise exc.BadRequest(errors)
- headers = self.get_success_headers(us.data)
- return response.Created(us.data, headers=headers)
-
- @detail_route(methods=['post'])
- @method_decorator(atomic)
- def milestone(self, request, *args, **kwargs):
- project = self.get_object_or_none()
- self.check_permissions(request, 'import_item', project)
-
- milestone = service.store_milestone(project, request.DATA.copy())
-
- errors = service.get_errors()
- if errors:
- raise exc.BadRequest(errors)
-
- headers = self.get_success_headers(milestone.data)
- return response.Created(milestone.data, headers=headers)
+ headers = self.get_success_headers(issue.data)
+ return response.Created(issue.data, headers=headers)
@detail_route(methods=['post'])
@method_decorator(atomic)
@@ -269,9 +269,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
project = self.get_object_or_none()
self.check_permissions(request, 'import_item', project)
- wiki_page = service.store_wiki_page(project, request.DATA.copy())
+ wiki_page = services.store.store_wiki_page(project, request.DATA.copy())
- errors = service.get_errors()
+ errors = services.store.get_errors()
if errors:
raise exc.BadRequest(errors)
@@ -284,9 +284,9 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
project = self.get_object_or_none()
self.check_permissions(request, 'import_item', project)
- wiki_link = service.store_wiki_link(project, request.DATA.copy())
+ wiki_link = services.store.store_wiki_link(project, request.DATA.copy())
- errors = service.get_errors()
+ errors = services.store.get_errors()
if errors:
raise exc.BadRequest(errors)
@@ -327,7 +327,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
total_memberships = len([m for m in dump.get("memberships", [])
if m.get("email", None) != dump["owner"]])
total_memberships = total_memberships + 1 # 1 is the owner
- (enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
+ (enough_slots, error_message) = users_services.has_available_slot_for_import_new_project(
user,
is_private,
total_memberships
@@ -335,11 +335,23 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
if not enough_slots:
raise exc.NotEnoughSlotsForProject(is_private, total_memberships, error_message)
+ # Async mode
if settings.CELERY_ENABLED:
task = tasks.load_project_dump.delay(user, dump)
return response.Accepted({"import_id": task.id})
- project = dump_service.dict_to_project(dump, request.user)
- response_data = ProjectSerializer(project).data
- return response.Created(response_data)
+ # Sync mode
+ try:
+ project = services.store_project_from_dict(dump, request.user)
+ except err.TaigaImportError as e:
+ # On Error
+ ## remove project
+ if e.project:
+ e.project.delete_related_content()
+ e.project.delete()
+ return response.BadRequest({"error": e.message, "details": e.errors})
+ else:
+ # On Success
+ response_data = ProjectSerializer(project).data
+ return response.Created(response_data)
diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py
deleted file mode 100644
index 243b9167..00000000
--- a/taiga/export_import/dump_service.py
+++ /dev/null
@@ -1,202 +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 django.utils.decorators import method_decorator
-from django.utils.translation import ugettext as _
-
-from taiga.projects.models import Membership, Project
-from taiga.users import services as users_service
-
-from . import serializers
-from . import service
-
-
-class TaigaImportError(Exception):
- def __init__(self, message):
- self.message = message
-
-
-def store_milestones(project, data):
- results = []
- for milestone_data in data.get("milestones", []):
- milestone = service.store_milestone(project, milestone_data)
- results.append(milestone)
- return results
-
-
-def store_tasks(project, data):
- results = []
- for task in data.get("tasks", []):
- task = service.store_task(project, task)
- results.append(task)
- return results
-
-
-def store_wiki_pages(project, data):
- results = []
- for wiki_page in data.get("wiki_pages", []):
- results.append(service.store_wiki_page(project, wiki_page))
- return results
-
-
-def store_wiki_links(project, data):
- results = []
- for wiki_link in data.get("wiki_links", []):
- results.append(service.store_wiki_link(project, wiki_link))
- return results
-
-
-def store_user_stories(project, data):
- results = []
- for userstory in data.get("user_stories", []):
- us = service.store_user_story(project, userstory)
- results.append(us)
- return results
-
-
-def store_timeline_entries(project, data):
- results = []
- for timeline in data.get("timeline", []):
- tl = service.store_timeline_entry(project, timeline)
- results.append(tl)
- return results
-
-
-def store_issues(project, data):
- issues = []
- for issue in data.get("issues", []):
- issues.append(service.store_issue(project, issue))
- return issues
-
-
-def store_tags_colors(project, data):
- project.tags_colors = data.get("tags_colors", [])
- project.save()
- return None
-
-
-def dict_to_project(data, owner=None):
- if owner:
- data["owner"] = owner.email
-
- # Validate if the owner can have this project
- 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
- (enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
- owner,
- is_private,
- total_memberships
- )
- if not enough_slots:
- raise TaigaImportError(error_message)
-
- project_serialized = service.store_project(data)
-
- if not project_serialized:
- raise TaigaImportError(_("error importing project data"))
-
- proj = project_serialized.object
-
- service.store_choices(proj, data, "points", serializers.PointsExportSerializer)
- service.store_choices(proj, data, "issue_types", serializers.IssueTypeExportSerializer)
- service.store_choices(proj, data, "issue_statuses", serializers.IssueStatusExportSerializer)
- service.store_choices(proj, data, "us_statuses", serializers.UserStoryStatusExportSerializer)
- service.store_choices(proj, data, "task_statuses", serializers.TaskStatusExportSerializer)
- service.store_choices(proj, data, "priorities", serializers.PriorityExportSerializer)
- service.store_choices(proj, data, "severities", serializers.SeverityExportSerializer)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing lists of project attributes"))
-
- service.store_default_choices(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing default project attributes values"))
-
- service.store_custom_attributes(proj, data, "userstorycustomattributes",
- serializers.UserStoryCustomAttributeExportSerializer)
- service.store_custom_attributes(proj, data, "taskcustomattributes",
- serializers.TaskCustomAttributeExportSerializer)
- service.store_custom_attributes(proj, data, "issuecustomattributes",
- serializers.IssueCustomAttributeExportSerializer)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing custom attributes"))
-
- service.store_roles(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing roles"))
-
- service.store_memberships(proj, data)
-
- if proj.memberships.filter(user=proj.owner).count() == 0:
- if proj.roles.all().count() > 0:
- Membership.objects.create(
- project=proj,
- email=proj.owner.email,
- user=proj.owner,
- role=proj.roles.all().first(),
- is_admin=True
- )
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing memberships"))
-
- store_milestones(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing sprints"))
-
- store_wiki_pages(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing wiki pages"))
-
- store_wiki_links(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing wiki links"))
-
- store_issues(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing issues"))
-
- store_user_stories(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing user stories"))
-
- store_tasks(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing tasks"))
-
- store_tags_colors(proj, data)
-
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing tags"))
-
- store_timeline_entries(proj, data)
- if service.get_errors(clear=False):
- raise TaigaImportError(_("error importing timelines"))
-
- proj.refresh_totals()
- return proj
diff --git a/taiga/export_import/exceptions.py b/taiga/export_import/exceptions.py
new file mode 100644
index 00000000..623d5b24
--- /dev/null
+++ b/taiga/export_import/exceptions.py
@@ -0,0 +1,23 @@
+# 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 .
+
+
+class TaigaImportError(Exception):
+ def __init__(self, message, project, errors=[]):
+ self.message = message
+ self.project = project
+ self.errors = errors
diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py
index daf6f6d5..dc59a17b 100644
--- a/taiga/export_import/management/commands/dump_project.py
+++ b/taiga/export_import/management/commands/dump_project.py
@@ -19,7 +19,7 @@ from django.core.management.base import BaseCommand, CommandError
from taiga.projects.models import Project
from taiga.export_import.renderers import ExportRenderer
-from taiga.export_import.service import render_project
+from taiga.export_import.services import render_project
import resource
diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py
index 367a2401..a1d919f0 100644
--- a/taiga/export_import/management/commands/load_dump.py
+++ b/taiga/export_import/management/commands/load_dump.py
@@ -21,10 +21,10 @@ from django.db.models import signals
from optparse import make_option
from taiga.base.utils import json
-from taiga.projects.models import Project
+from taiga.export_import.import services
+from taiga.export_import.exceptions as err
from taiga.export_import.renderers import ExportRenderer
-from taiga.export_import.dump_service import dict_to_project, TaigaImportError
-from taiga.export_import.service import get_errors
+from taiga.projects.models import Project
from taiga.users.models import User
@@ -61,8 +61,12 @@ class Command(BaseCommand):
signals.post_delete.receivers = receivers_back
user = User.objects.get(email=args[1])
- dict_to_project(data, user)
- except TaigaImportError as e:
+ services.store_project_from_dict(data, user)
+ except err.TaigaImportError as e:
+ if e.project:
+ e.project.delete_related_content()
+ e.project.delete()
+
print("ERROR:", end=" ")
print(e.message)
- print(get_errors())
+ print(services.store.get_errors())
diff --git a/taiga/export_import/services/__init__.py b/taiga/export_import/services/__init__.py
new file mode 100644
index 00000000..8aad0f08
--- /dev/null
+++ b/taiga/export_import/services/__init__.py
@@ -0,0 +1,26 @@
+# 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 .
+
+# This makes all code that import services works and
+# is not the baddest practice ;)
+
+from .render import render_project
+from . import render
+
+from .store import store_project_from_dict
+from . import store
+
diff --git a/taiga/export_import/services/render.py b/taiga/export_import/services/render.py
new file mode 100644
index 00000000..b9905baf
--- /dev/null
+++ b/taiga/export_import/services/render.py
@@ -0,0 +1,124 @@
+# 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 .
+
+# 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.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):
+ serializer = serializers.ProjectExportSerializer(project)
+ outfile.write('{\n')
+
+ first_field = True
+ for field_name in serializer.fields.keys():
+ # Avoid writing "," in the last element
+ if not first_field:
+ outfile.write(",\n")
+ else:
+ first_field = False
+
+ field = serializer.fields.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)
+ outfile.write('"{}": [\n'.format(field_name))
+
+ 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
+ if not first_item:
+ outfile.write(",\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)
+
+ first_attachment = True
+ for attachment in item.attachments.iterator():
+ # Avoid writing "," in the last element
+ if not first_attachment:
+ outfile.write(",\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)
+
+ # 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).decode('utf-8')
+ outfile.write(b64_data)
+
+ outfile.write('", \n "name":"{}"}}\n}}'.format(
+ os.path.basename(attachment_file.name)))
+
+ outfile.write(']}')
+ outfile.flush()
+ gc.collect()
+ outfile.write(']')
+
+ else:
+ value = field.field_to_native(project, field_name)
+ outfile.write('"{}": {}'.format(field_name, json.dumps(value)))
+
+ # Generate the timeline
+ outfile.write(',\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")
+ else:
+ first_timeline = False
+
+ dumped_value = json.dumps(serializers.TimelineExportSerializer(timeline_item).data)
+ outfile.write(dumped_value)
+
+ outfile.write(']}\n')
+
diff --git a/taiga/export_import/service.py b/taiga/export_import/services/store.py
similarity index 63%
rename from taiga/export_import/service.py
rename to taiga/export_import/services/store.py
index 14ecd22d..e286c97c 100644
--- a/taiga/export_import/service.py
+++ b/taiga/export_import/services/store.py
@@ -15,30 +15,35 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import base64
-import gc
-import resource
+# This makes all code that import services works and
+# is not the baddest practice ;)
+
import os
-import os.path as path
import uuid
from unidecode import unidecode
-from django.template.defaultfilters import slugify
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
-from django.core.files.storage import default_storage
+from django.template.defaultfilters import slugify
+from django.utils.translation import ugettext as _
-from taiga.base.utils import json
from taiga.projects.history.services import make_key_from_model_object, take_snapshot
-from taiga.timeline.service import build_project_namespace, get_project_timeline
+from taiga.projects.models import Membership
from taiga.projects.references import sequences as seq
from taiga.projects.references import models as refs
from taiga.projects.userstories.models import RolePoints
from taiga.projects.services import find_invited_user
-from taiga.base.api.fields import get_component
+from taiga.timeline.service import build_project_namespace
+from taiga.users import services as users_service
-from . import serializers
+from .. import exceptions as err
+from .. import serializers
+
+
+########################################################################
+## Manage errors
+########################################################################
_errors_log = {}
@@ -57,97 +62,16 @@ def add_errors(section, errors):
_errors_log[section] = [errors]
-def render_project(project, outfile, chunk_size = 8190):
- serializer = serializers.ProjectExportSerializer(project)
- outfile.write('{\n')
-
- first_field = True
- for field_name in serializer.fields.keys():
- # Avoid writing "," in the last element
- if not first_field:
- outfile.write(",\n")
- else:
- first_field = False
-
- field = serializer.fields.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)
- outfile.write('"{}": [\n'.format(field_name))
-
- 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
- if not first_item:
- outfile.write(",\n")
- else:
- first_item = False
+def reset_errors():
+ _errors_log.clear()
- dumped_value = json.dumps(field.to_native(item))
- writing_value = dumped_value[:-1]+ ',\n "attachments": [\n'
- outfile.write(writing_value)
+########################################################################
+## Store functions
+########################################################################
- first_attachment = True
- for attachment in item.attachments.iterator():
- # Avoid writing "," in the last element
- if not first_attachment:
- outfile.write(",\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)
-
- # 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).decode('utf-8')
- outfile.write(b64_data)
-
- outfile.write('", \n "name":"{}"}}\n}}'.format(
- os.path.basename(attachment_file.name)))
-
- outfile.write(']}')
- outfile.flush()
- gc.collect()
- outfile.write(']')
-
- else:
- value = field.field_to_native(project, field_name)
- outfile.write('"{}": {}'.format(field_name, json.dumps(value)))
-
- # Generate the timeline
- outfile.write(',\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")
- else:
- first_timeline = False
-
- dumped_value = json.dumps(serializers.TimelineExportSerializer(timeline_item).data)
- outfile.write(dumped_value)
-
- outfile.write(']}\n')
+## PROJECT
def store_project(data):
project_data = {}
@@ -175,43 +99,19 @@ def store_project(data):
return None
-def _store_choice(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)
- return None
+## MISC
+
+def _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, values):
+ ret = {}
+ for attr in custom_attributes:
+ value = values.get(attr["name"], None)
+ if value is not None:
+ ret[str(attr["id"])] = value
+
+ return ret
-def store_choices(project, data, field, serializer):
- result = []
- for choice_data in data.get(field, []):
- result.append(_store_choice(project, choice_data, field, serializer))
- return result
-
-
-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)
- return None
-
-
-def store_custom_attributes(project, data, field, serializer):
- result = []
- for custom_attribute_data in data.get(field, []):
- result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer))
- return result
-
-
-def store_custom_attributes_values(obj, data_values, obj_field, serializer_class):
+def _store_custom_attributes_values(obj, data_values, obj_field, serializer_class):
data = {
obj_field: obj.id,
"attributes_values": data_values,
@@ -231,17 +131,39 @@ def store_custom_attributes_values(obj, data_values, obj_field, serializer_class
return None
-def _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, values):
- ret = {}
- for attr in custom_attributes:
- value = values.get(attr["name"], None)
- if value is not None:
- ret[str(attr["id"])] = value
-
- return ret
+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
-def store_role(project, role):
+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
+
+
+## ROLES
+
+def _store_role(project, role):
serialized = serializers.RoleExportSerializer(data=role)
if serialized.is_valid():
serialized.object.project = project
@@ -255,14 +177,60 @@ def store_role(project, role):
def store_roles(project, data):
results = []
for role in data.get("roles", []):
- serialized = store_role(project, role)
+ serialized = _store_role(project, role)
if serialized:
results.append(serialized)
return results
-def store_default_choices(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
+
+ add_errors("memberships", serialized.errors)
+ return None
+
+
+def store_memberships(project, data):
+ results = []
+ for membership in data.get("memberships", []):
+ results.append(_store_membership(project, membership))
+ return results
+
+
+## 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)
+ return None
+
+
+def store_project_attributes_values(project, data, field, serializer):
+ result = []
+ for choice_data in data.get(field, []):
+ result.append(_store_project_attribute_value(project, choice_data, field, serializer))
+ return result
+
+
+## DEFAULT PROJECT ATTRIBUTES VALUES
+
+def store_default_project_attributes_values(project, data):
def helper(project, field, related, data):
if field in data:
value = related.all().get(name=data[field])
@@ -281,75 +249,27 @@ def store_default_choices(project, data):
project.save()
-def store_membership(project, membership):
- serialized = serializers.MembershipExportSerializer(data=membership, context={"project": project})
+## 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.object.token = str(uuid.uuid1())
- serialized.object.user = find_invited_user(serialized.object.email,
- default=serialized.object.user)
serialized.save()
- return serialized
-
- add_errors("memberships", serialized.errors)
+ return serialized.object
+ add_errors(field, serialized.errors)
return None
-def store_memberships(project, data):
- results = []
- for membership in data.get("memberships", []):
- results.append(store_membership(project, membership))
- return results
+def store_custom_attributes(project, data, field, serializer):
+ result = []
+ for custom_attribute_data in data.get(field, []):
+ result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer))
+ return result
-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
-
- serialized.save()
- serialized.save_watchers()
-
- if serialized.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)
- else:
- serialized.object.ref, _ = refs.make_reference(serialized.object, project)
- serialized.object.save()
-
- for task_attachment in data.get("attachments", []):
- store_attachment(project, serialized.object, task_attachment)
-
- history_entries = data.get("history", [])
- for history in history_entries:
- store_history(project, serialized.object, history)
-
- if not history_entries:
- take_snapshot(serialized.object, user=serialized.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_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)
-
- return serialized
-
- add_errors("tasks", serialized.errors)
- return None
-
+## MILESTONE
def store_milestone(project, milestone):
serialized = serializers.MilestoneExportSerializer(data=milestone, project=project)
@@ -368,90 +288,17 @@ def store_milestone(project, milestone):
return None
-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 = path.basename(serialized.object.attached_file.name)
- serialized.save()
- return serialized
- add_errors("attachments", serialized.errors)
- return serialized
+def store_milestones(project, data):
+ results = []
+ for milestone_data in data.get("milestones", []):
+ milestone = store_milestone(project, milestone_data)
+ results.append(milestone)
+ return results
-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._importing = True
- serialized.save()
- return serialized
- add_errors("timeline", serialized.errors)
- return serialized
+## USER STORIES
-
-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
-
-
-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()
-
- for attachment in wiki_page.get("attachments", []):
- store_attachment(project, serialized.object, attachment)
-
- history_entries = wiki_page.get("history", [])
- for history in history_entries:
- store_history(project, serialized.object, history)
-
- if not history_entries:
- take_snapshot(serialized.object, user=serialized.object.owner)
-
- return serialized
-
- add_errors("wiki_pages", serialized.errors)
- return None
-
-
-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
-
- add_errors("wiki_links", serialized.errors)
- return None
-
-
-def store_role_point(project, us, role_point):
+def _store_role_point(project, us, role_point):
serialized = serializers.RolePointsExportSerializer(data=role_point, context={"project": project})
if serialized.is_valid():
try:
@@ -468,7 +315,6 @@ def store_role_point(project, us, role_point):
add_errors("role_points", serialized.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
@@ -497,14 +343,14 @@ def store_user_story(project, data):
serialized.object.save()
for us_attachment in data.get("attachments", []):
- store_attachment(project, serialized.object, us_attachment)
+ _store_attachment(project, serialized.object, us_attachment)
for role_point in data.get("role_points", []):
- store_role_point(project, serialized.object, role_point)
+ _store_role_point(project, serialized.object, role_point)
history_entries = data.get("history", [])
for history in history_entries:
- store_history(project, serialized.object, history)
+ _store_history(project, serialized.object, history)
if not history_entries:
take_snapshot(serialized.object, user=serialized.object.owner)
@@ -514,7 +360,7 @@ def store_user_story(project, data):
custom_attributes = serialized.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,
+ _store_custom_attributes_values(serialized.object, custom_attributes_values,
"user_story", serializers.UserStoryCustomAttributesValuesExportSerializer)
return serialized
@@ -523,6 +369,74 @@ def store_user_story(project, data):
return None
+def store_user_stories(project, data):
+ results = []
+ for userstory in data.get("user_stories", []):
+ us = store_user_story(project, userstory)
+ results.append(us)
+ return results
+
+
+## TASKS
+
+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
+
+ serialized.save()
+ serialized.save_watchers()
+
+ if serialized.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)
+ else:
+ serialized.object.ref, _ = refs.make_reference(serialized.object, project)
+ serialized.object.save()
+
+ for task_attachment in data.get("attachments", []):
+ _store_attachment(project, serialized.object, task_attachment)
+
+ history_entries = data.get("history", [])
+ for history in history_entries:
+ _store_history(project, serialized.object, history)
+
+ if not history_entries:
+ take_snapshot(serialized.object, user=serialized.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_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)
+
+ return serialized
+
+ add_errors("tasks", serialized.errors)
+ return None
+
+
+def store_tasks(project, data):
+ results = []
+ for task in data.get("tasks", []):
+ task = store_task(project, task)
+ results.append(task)
+ return results
+
+
+## ISSUES
+
def store_issue(project, data):
serialized = serializers.IssueExportSerializer(data=data, context={"project": project})
@@ -558,11 +472,11 @@ def store_issue(project, data):
serialized.object.save()
for attachment in data.get("attachments", []):
- store_attachment(project, serialized.object, attachment)
+ _store_attachment(project, serialized.object, attachment)
history_entries = data.get("history", [])
for history in history_entries:
- store_history(project, serialized.object, history)
+ _store_history(project, serialized.object, history)
if not history_entries:
take_snapshot(serialized.object, user=serialized.object.owner)
@@ -572,10 +486,248 @@ def store_issue(project, data):
custom_attributes = serialized.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,
+ _store_custom_attributes_values(serialized.object, custom_attributes_values,
"issue", serializers.IssueCustomAttributesValuesExportSerializer)
return serialized
add_errors("issues", serialized.errors)
return None
+
+
+def store_issues(project, data):
+ issues = []
+ for issue in data.get("issues", []):
+ issues.append(store_issue(project, issue))
+ return issues
+
+
+## WIKI PAGES
+
+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()
+
+ for attachment in wiki_page.get("attachments", []):
+ _store_attachment(project, serialized.object, attachment)
+
+ history_entries = wiki_page.get("history", [])
+ for history in history_entries:
+ _store_history(project, serialized.object, history)
+
+ if not history_entries:
+ take_snapshot(serialized.object, user=serialized.object.owner)
+
+ return serialized
+
+ add_errors("wiki_pages", serialized.errors)
+ return None
+
+
+def store_wiki_pages(project, data):
+ results = []
+ for wiki_page in data.get("wiki_pages", []):
+ results.append(store_wiki_page(project, wiki_page))
+ return results
+
+
+## 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
+
+ add_errors("wiki_links", serialized.errors)
+ return None
+
+
+def store_wiki_links(project, data):
+ results = []
+ for wiki_link in data.get("wiki_links", []):
+ results.append(store_wiki_link(project, wiki_link))
+ return results
+
+
+## TAGS COLORS
+
+def store_tags_colors(project, data):
+ project.tags_colors = data.get("tags_colors", [])
+ project.save()
+ return None
+
+
+## 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._importing = True
+ serialized.save()
+ return serialized
+ add_errors("timeline", serialized.errors)
+ return serialized
+
+
+def store_timeline_entries(project, data):
+ results = []
+ for timeline in data.get("timeline", []):
+ tl = _store_timeline_entry(project, timeline)
+ results.append(tl)
+ return results
+
+
+#############################################
+## Store project dict
+#############################################
+
+
+def _validate_if_owner_have_enought_space_to_this_project(owner, data):
+ # Validate if the owner can have this project
+ data["owner"] = owner.email
+
+ 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
+ (enough_slots, error_message) = users_service.has_available_slot_for_import_new_project(
+ owner,
+ is_private,
+ total_memberships
+ )
+ if not enough_slots:
+ raise err.TaigaImportError(error_message, None)
+
+
+def _create_project_object(data):
+ # Create the project
+ project_serialized = store_project(data)
+
+ if not project_serialized:
+ raise err.TaigaImportError(_("error importing project data"), None)
+
+ return project_serialized.object if project_serialized else None
+
+
+def _create_membership_for_project_owner(project):
+ if project.memberships.filter(user=project.owner).count() == 0:
+ if project.roles.all().count() > 0:
+ Membership.objects.create(
+ project=project,
+ email=project.owner.email,
+ user=project.owner,
+ role=project.roles.all().first(),
+ is_admin=True
+ )
+
+
+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)
+ if errors:
+ raise err.TaigaImportError(message, project, errors=errors)
+
+ # Create roles
+ store_roles(project, data)
+ check_if_there_is_some_error(_("error importing roles"), None)
+
+ # Create memberships
+ store_memberships(project, data)
+ _create_membership_for_project_owner(project)
+ 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)
+ check_if_there_is_some_error(_("error importing lists of project attributes"), project)
+
+ # Create default values for project attributes
+ store_default_project_attributes_values(project, data)
+ check_if_there_is_some_error(_("error importing default project attributes values"), project)
+
+ # Create custom attributes
+ store_custom_attributes(project, data, "userstorycustomattributes",
+ serializers.UserStoryCustomAttributeExportSerializer)
+ store_custom_attributes(project, data, "taskcustomattributes",
+ serializers.TaskCustomAttributeExportSerializer)
+ store_custom_attributes(project, data, "issuecustomattributes",
+ 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 user stories
+ store_user_stories(project, data)
+ check_if_there_is_some_error(_("error importing user stories"), project)
+
+ # Createer tasks
+ 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)
+
+ # Create wiki links
+ store_wiki_links(project, data)
+ check_if_there_is_some_error(_("error importing wiki links"), project)
+
+ # Create tags
+ store_tags_colors(project, data)
+ check_if_there_is_some_error(_("error importing tags"), project)
+
+ # Create timeline
+ store_timeline_entries(project, data)
+ check_if_there_is_some_error(_("error importing timelines"), project)
+
+ # Regenerate stats
+ project.refresh_totals()
+
+
+def store_project_from_dict(data, owner=None):
+ reset_errors()
+
+ # Validate
+ if owner:
+ _validate_if_owner_have_enought_space_to_this_project(owner, data)
+
+ # Create project
+ project = _create_project_object(data)
+
+ # Populate project
+ try:
+ _populate_project_object(project, data)
+ except err.TaigaImportError:
+ # reraise known inport errors
+ raise
+ except:
+ # reise unknown errors as import error
+ raise err.TaigaImportError(_("unexpected error importing project"), project)
+
+ return project
diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py
index 79880ba4..4e5012d1 100644
--- a/taiga/export_import/tasks.py
+++ b/taiga/export_import/tasks.py
@@ -27,10 +27,11 @@ from django.conf import settings
from django.utils.translation import ugettext as _
from taiga.base.mails import mail_builder
+from taiga.base.utils import json
from taiga.celery import app
-from .service import render_project
-from .dump_service import dict_to_project
+from . import exceptions as err
+from . import services
from .renderers import ExportRenderer
logger = logging.getLogger('taiga.export_import')
@@ -46,7 +47,7 @@ def dump_project(self, user, project):
try:
url = default_storage.url(path)
with default_storage.open(storage_path, mode="w") as outfile:
- render_project(project, outfile)
+ services.render_project(project, outfile)
except Exception:
# Error
@@ -77,28 +78,57 @@ def delete_project_dump(project_id, project_slug, task_id):
default_storage.delete("exports/{}/{}-{}.json".format(project_id, project_slug, task_id))
+ADMIN_ERROR_LOAD_PROJECT_DUMP_MESSAGE = _("""
+
+Error loading dump by {user_full_name} <{user_email}>:"
+
+
+REASON:
+-------
+{reason}
+
+DETAILS:
+--------
+{details}
+
+TRACE ERROR:
+------------""")
+
+
@app.task
def load_project_dump(user, dump):
try:
- project = dict_to_project(dump, user)
- except Exception:
- # Error
+ project = services.store_project_from_dict(dump, user)
+ except err.TaigaImportError as e:
+ # On Error
+ ## remove project
+ if e.project:
+ e.project.delete_related_content()
+ e.project.delete()
+
+ ## send email to the user
+ error_subject = _("Error loading project dump")
+ error_message = e.message or _("Error loading your project dump file")
+
ctx = {
"user": user,
- "error_subject": _("Error loading project dump"),
- "error_message": _("Error loading project dump"),
+ "error_subject": error_message,
+ "error_message": error_subject,
}
email = mail_builder.import_error(user, ctx)
email.send()
- logger.error('Error loading dump by %s <%s>',
- user,
- user.email,
- exc_info=sys.exc_info())
- # TODO: [Rollback] Remove project because it can be corrupted
+ ## logged the error to sysadmins
+ text = ADMIN_ERROR_LOAD_PROJECT_DUMP_MESSAGE.format(
+ user_full_name=user,
+ user_email=user.email,
+ reason=e.message or _(" -- no detail info --"),
+ details=json.dumps(e.errors, indent=4)
+ )
+ logger.error(text, exc_info=sys.exc_info())
else:
- # Success
+ # On Success
ctx = {"user": user, "project": project}
email = mail_builder.load_dump(user, ctx)
email.send()
diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py
index a881e67d..b9e2d1c7 100644
--- a/tests/integration/test_importer_api.py
+++ b/tests/integration/test_importer_api.py
@@ -23,7 +23,8 @@ from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile
from taiga.base.utils import json
-from taiga.export_import.dump_service import dict_to_project, TaigaImportError
+from taiga.export_import import services
+from taiga.export_import.exceptions import TaigaImportError
from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue
from taiga.projects.userstories.models import UserStory
@@ -36,6 +37,11 @@ from ..utils import DUMMY_BMP_DATA
pytestmark = pytest.mark.django_db
+
+#######################################################
+## test api/v1/importer
+#######################################################
+
def test_invalid_project_import(client):
user = f.UserFactory.create()
client.login(user)
@@ -43,7 +49,7 @@ def test_invalid_project_import(client):
url = reverse("importer-list")
data = {}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
@@ -60,17 +66,16 @@ def test_valid_project_import_without_extra_data(client):
"watchers": ["testing@taiga.io"]
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
must_empty_children = [
"issues", "user_stories", "us_statuses", "wiki_pages", "priorities",
"severities", "milestones", "points", "issue_types", "task_statuses",
"issue_statuses", "wiki_links",
]
- assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
- assert response_data["owner"] == user.email
- assert response_data["watchers"] == [user.email, user_watching.email]
+ assert all(map(lambda x: len(response.data[x]) == 0, must_empty_children))
+ assert response.data["owner"] == user.email
+ assert response.data["watchers"] == [user.email, user_watching.email]
def test_valid_project_without_enough_public_projects_slots(client):
@@ -170,11 +175,10 @@ def test_valid_project_import_with_not_existing_memberships(client):
"roles": [{"name": "Role"}]
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
# The new membership and the owner membership
- assert len(response_data["memberships"]) == 2
+ assert len(response.data["memberships"]) == 2
def test_valid_project_import_with_membership_uuid_rewrite(client):
@@ -193,9 +197,8 @@ def test_valid_project_import_with_membership_uuid_rewrite(client):
"roles": [{"name": "Role"}]
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
assert Membership.objects.filter(email="with-uuid@email.com", token="123").count() == 0
@@ -234,9 +237,8 @@ def test_valid_project_import_with_extra_data(client):
}],
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
must_empty_children = [
"issues", "user_stories", "wiki_pages", "milestones",
"wiki_links",
@@ -247,10 +249,10 @@ def test_valid_project_import_with_extra_data(client):
"issue_types", "task_statuses", "issue_statuses", "memberships",
]
- assert all(map(lambda x: len(response_data[x]) == 0, must_empty_children))
+ assert all(map(lambda x: len(response.data[x]) == 0, must_empty_children))
# Allwais is created at least the owner membership
- assert all(map(lambda x: len(response_data[x]) == 1, must_one_instance_children))
- assert response_data["owner"] == user.email
+ assert all(map(lambda x: len(response.data[x]) == 1, must_one_instance_children))
+ assert response.data["owner"] == user.email
def test_invalid_project_import_without_roles(client):
@@ -263,10 +265,9 @@ def test_invalid_project_import_without_roles(client):
"description": "Imported project",
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 2
+ assert len(response.data) == 2
assert Project.objects.filter(slug="imported-project").count() == 0
def test_invalid_project_import_with_extra_data(client):
@@ -290,10 +291,9 @@ def test_invalid_project_import_with_extra_data(client):
"issue_statuses": [{}],
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 7
+ assert len(response.data) == 7
assert Project.objects.filter(slug="imported-project").count() == 0
@@ -360,6 +360,327 @@ def test_invalid_project_import_with_custom_attributes(client):
assert Project.objects.filter(slug="imported-project").count() == 0
+#######################################################
+## tes api/v1/importer/milestone
+#######################################################
+
+def test_invalid_milestone_import(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ client.login(user)
+
+ url = reverse("importer-milestone", args=[project.pk])
+ data = {}
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+
+
+def test_valid_milestone_import(client):
+ user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ client.login(user)
+
+ url = reverse("importer-milestone", args=[project.pk])
+ data = {
+ "name": "Imported milestone",
+ "estimated_start": "2014-10-10",
+ "estimated_finish": "2014-10-20",
+ "watchers": ["testing@taiga.io"]
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert response.data["watchers"] == [user_watching.email]
+
+def test_milestone_import_duplicated_milestone(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ client.login(user)
+
+ url = reverse("importer-milestone", args=[project.pk])
+ data = {
+ "name": "Imported milestone",
+ "estimated_start": "2014-10-10",
+ "estimated_finish": "2014-10-20",
+ }
+ # We create twice the same milestone
+ response = client.json.post(url, json.dumps(data))
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert response.data["milestones"][0]["name"][0] == "Name duplicated for the project"
+
+
+
+#######################################################
+## tes api/v1/importer/us
+#######################################################
+
+def test_invalid_us_import(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ client.login(user)
+
+ url = reverse("importer-us", args=[project.pk])
+ data = {}
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+
+
+def test_valid_us_import_without_extra_data(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_us_status = f.UserStoryStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-us", args=[project.pk])
+ data = {
+ "subject": "Test"
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert response.data["owner"] == user.email
+ assert response.data["ref"] is not None
+
+
+def test_valid_us_import_with_extra_data(client):
+ user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_us_status = f.UserStoryStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-us", args=[project.pk])
+ data = {
+ "subject": "Imported us",
+ "description": "Imported us",
+ "attachments": [{
+ "owner": user.email,
+ "attached_file": {
+ "name": "imported attachment",
+ "data": base64.b64encode(b"TEST").decode("utf-8")
+ }
+ }],
+ "watchers": ["testing@taiga.io"]
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert len(response.data["attachments"]) == 1
+ assert response.data["owner"] == user.email
+ assert response.data["ref"] is not None
+ assert response.data["watchers"] == [user_watching.email]
+
+
+def test_invalid_us_import_with_extra_data(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_us_status = f.UserStoryStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-us", args=[project.pk])
+ data = {
+ "subject": "Imported us",
+ "description": "Imported us",
+ "attachments": [{}],
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert len(response.data) == 1
+ assert UserStory.objects.filter(subject="Imported us").count() == 0
+
+
+def test_invalid_us_import_with_bad_choices(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_us_status = f.UserStoryStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-us", args=[project.pk])
+ data = {
+ "subject": "Imported us",
+ "description": "Imported us",
+ "status": "Not valid"
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert len(response.data) == 1
+
+
+#######################################################
+## tes api/v1/importer/task
+#######################################################
+
+def test_invalid_task_import(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ client.login(user)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {}
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+
+
+def test_valid_task_import_without_extra_data(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_task_status = f.TaskStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {
+ "subject": "Test"
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert response.data["owner"] == user.email
+ assert response.data["ref"] is not None
+
+
+def test_valid_task_import_with_custom_attributes_values(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ membership = f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_task_status = f.TaskStatusFactory.create(project=project)
+ project.save()
+ custom_attr = f.TaskCustomAttributeFactory(project=project)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {
+ "subject": "Test Custom Attrs Values Tasks",
+ "custom_attributes_values": {
+ custom_attr.name: "test_value"
+ }
+ }
+
+ client.login(user)
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ custom_attributes_values = apps.get_model("custom_attributes.TaskCustomAttributesValues").objects.get(
+ task__subject=response.data["subject"])
+ assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"}
+
+
+def test_valid_task_import_with_extra_data(client):
+ user = f.UserFactory.create()
+ user_watching = f.UserFactory.create(email="testing@taiga.io")
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_task_status = f.TaskStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {
+ "subject": "Imported task",
+ "description": "Imported task",
+ "attachments": [{
+ "owner": user.email,
+ "attached_file": {
+ "name": "imported attachment",
+ "data": base64.b64encode(b"TEST").decode("utf-8")
+ }
+ }],
+ "watchers": ["testing@taiga.io"]
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert len(response.data["attachments"]) == 1
+ assert response.data["owner"] == user.email
+ assert response.data["ref"] is not None
+ assert response.data["watchers"] == [user_watching.email]
+
+
+def test_invalid_task_import_with_extra_data(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_task_status = f.TaskStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {
+ "subject": "Imported task",
+ "description": "Imported task",
+ "attachments": [{}],
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert len(response.data) == 1
+ assert Task.objects.filter(subject="Imported task").count() == 0
+
+
+def test_invalid_task_import_with_bad_choices(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_task_status = f.TaskStatusFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {
+ "subject": "Imported task",
+ "description": "Imported task",
+ "status": "Not valid"
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 400
+ assert len(response.data) == 1
+
+
+def test_valid_task_with_user_story(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ f.MembershipFactory(project=project, user=user, is_admin=True)
+ project.default_task_status = f.TaskStatusFactory.create(project=project)
+ us = f.UserStoryFactory.create(project=project)
+ project.save()
+ client.login(user)
+
+ url = reverse("importer-task", args=[project.pk])
+ data = {
+ "subject": "Imported task",
+ "description": "Imported task",
+ "user_story": us.ref
+ }
+
+ response = client.json.post(url, json.dumps(data))
+ assert response.status_code == 201
+ assert us.tasks.all().count() == 1
+
+
+#######################################################
+## tes api/v1/importer/issue
+#######################################################
+
def test_invalid_issue_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@@ -369,7 +690,7 @@ def test_invalid_issue_import(client):
url = reverse("importer-issue", args=[project.pk])
data = {}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
@@ -387,11 +708,10 @@ def test_valid_user_story_import(client):
"finish_date": "2014-10-24T00:00:00+0000"
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
- assert response_data["subject"] == "Imported issue"
- assert response_data["finish_date"] == "2014-10-24T00:00:00+0000"
+ assert response.data["subject"] == "Imported issue"
+ assert response.data["finish_date"] == "2014-10-24T00:00:00+0000"
def test_valid_user_story_import_with_custom_attributes_values(client):
@@ -434,11 +754,10 @@ def test_valid_issue_import_without_extra_data(client):
"subject": "Test"
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
- assert response_data["owner"] == user.email
- assert response_data["ref"] is not None
+ assert response.data["owner"] == user.email
+ assert response.data["ref"] is not None
def test_valid_issue_import_with_custom_attributes_values(client):
@@ -495,14 +814,13 @@ def test_valid_issue_import_with_extra_data(client):
"watchers": ["testing@taiga.io"]
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
- assert len(response_data["attachments"]) == 1
- assert response_data["owner"] == user.email
- assert response_data["ref"] is not None
- assert response_data["finished_date"] == "2014-10-24T00:00:00+0000"
- assert response_data["watchers"] == [user_watching.email]
+ assert len(response.data["attachments"]) == 1
+ assert response.data["owner"] == user.email
+ assert response.data["ref"] is not None
+ assert response.data["finished_date"] == "2014-10-24T00:00:00+0000"
+ assert response.data["watchers"] == [user_watching.email]
def test_invalid_issue_import_with_extra_data(client):
@@ -523,10 +841,9 @@ def test_invalid_issue_import_with_extra_data(client):
"attachments": [{}],
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
+ assert len(response.data) == 1
assert Issue.objects.filter(subject="Imported issue").count() == 0
@@ -548,10 +865,9 @@ def test_invalid_issue_import_with_bad_choices(client):
"status": "Not valid"
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
+ assert len(response.data) == 1
url = reverse("importer-issue", args=[project.pk])
data = {
@@ -560,10 +876,9 @@ def test_invalid_issue_import_with_bad_choices(client):
"priority": "Not valid"
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
+ assert len(response.data) == 1
url = reverse("importer-issue", args=[project.pk])
data = {
@@ -572,10 +887,9 @@ def test_invalid_issue_import_with_bad_choices(client):
"severity": "Not valid"
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
+ assert len(response.data) == 1
url = reverse("importer-issue", args=[project.pk])
data = {
@@ -584,272 +898,14 @@ def test_invalid_issue_import_with_bad_choices(client):
"type": "Not valid"
}
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
-
-
-def test_invalid_us_import(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- client.login(user)
-
- url = reverse("importer-us", args=[project.pk])
- data = {}
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
-
-
-def test_valid_us_import_without_extra_data(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_us_status = f.UserStoryStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-us", args=[project.pk])
- data = {
- "subject": "Test"
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 201
- response_data = response.data
- assert response_data["owner"] == user.email
- assert response_data["ref"] is not None
-
-
-def test_valid_us_import_with_extra_data(client):
- user = f.UserFactory.create()
- user_watching = f.UserFactory.create(email="testing@taiga.io")
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_us_status = f.UserStoryStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-us", args=[project.pk])
- data = {
- "subject": "Imported us",
- "description": "Imported us",
- "attachments": [{
- "owner": user.email,
- "attached_file": {
- "name": "imported attachment",
- "data": base64.b64encode(b"TEST").decode("utf-8")
- }
- }],
- "watchers": ["testing@taiga.io"]
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 201
- response_data = response.data
- assert len(response_data["attachments"]) == 1
- assert response_data["owner"] == user.email
- assert response_data["ref"] is not None
- assert response_data["watchers"] == [user_watching.email]
-
-
-def test_invalid_us_import_with_extra_data(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_us_status = f.UserStoryStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-us", args=[project.pk])
- data = {
- "subject": "Imported us",
- "description": "Imported us",
- "attachments": [{}],
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
- assert UserStory.objects.filter(subject="Imported us").count() == 0
-
-
-def test_invalid_us_import_with_bad_choices(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_us_status = f.UserStoryStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-us", args=[project.pk])
- data = {
- "subject": "Imported us",
- "description": "Imported us",
- "status": "Not valid"
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
-
-
-def test_invalid_task_import(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- client.login(user)
-
- url = reverse("importer-task", args=[project.pk])
- data = {}
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
-
-
-def test_valid_task_import_without_extra_data(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_task_status = f.TaskStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-task", args=[project.pk])
- data = {
- "subject": "Test"
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 201
- response_data = response.data
- assert response_data["owner"] == user.email
- assert response_data["ref"] is not None
-
-
-def test_valid_task_import_with_custom_attributes_values(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- membership = f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_task_status = f.TaskStatusFactory.create(project=project)
- project.save()
- custom_attr = f.TaskCustomAttributeFactory(project=project)
-
- url = reverse("importer-task", args=[project.pk])
- data = {
- "subject": "Test Custom Attrs Values Tasks",
- "custom_attributes_values": {
- custom_attr.name: "test_value"
- }
- }
-
- client.login(user)
response = client.json.post(url, json.dumps(data))
- assert response.status_code == 201
- custom_attributes_values = apps.get_model("custom_attributes.TaskCustomAttributesValues").objects.get(
- task__subject=response.data["subject"])
- assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"}
-
-
-def test_valid_task_import_with_extra_data(client):
- user = f.UserFactory.create()
- user_watching = f.UserFactory.create(email="testing@taiga.io")
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_task_status = f.TaskStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-task", args=[project.pk])
- data = {
- "subject": "Imported task",
- "description": "Imported task",
- "attachments": [{
- "owner": user.email,
- "attached_file": {
- "name": "imported attachment",
- "data": base64.b64encode(b"TEST").decode("utf-8")
- }
- }],
- "watchers": ["testing@taiga.io"]
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 201
- response_data = response.data
- assert len(response_data["attachments"]) == 1
- assert response_data["owner"] == user.email
- assert response_data["ref"] is not None
- assert response_data["watchers"] == [user_watching.email]
-
-
-def test_invalid_task_import_with_extra_data(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_task_status = f.TaskStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-task", args=[project.pk])
- data = {
- "subject": "Imported task",
- "description": "Imported task",
- "attachments": [{}],
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
- assert Task.objects.filter(subject="Imported task").count() == 0
+ assert len(response.data) == 1
-def test_invalid_task_import_with_bad_choices(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_task_status = f.TaskStatusFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-task", args=[project.pk])
- data = {
- "subject": "Imported task",
- "description": "Imported task",
- "status": "Not valid"
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
-
-
-def test_valid_task_with_user_story(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- project.default_task_status = f.TaskStatusFactory.create(project=project)
- us = f.UserStoryFactory.create(project=project)
- project.save()
- client.login(user)
-
- url = reverse("importer-task", args=[project.pk])
- data = {
- "subject": "Imported task",
- "description": "Imported task",
- "user_story": us.ref
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 201
- assert us.tasks.all().count() == 1
-
+#######################################################
+## tes api/v1/importer/wiki-page
+#######################################################
def test_invalid_wiki_page_import(client):
user = f.UserFactory.create()
@@ -860,7 +916,7 @@ def test_invalid_wiki_page_import(client):
url = reverse("importer-wiki-page", args=[project.pk])
data = {}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
@@ -875,10 +931,9 @@ def test_valid_wiki_page_import_without_extra_data(client):
"slug": "imported-wiki-page",
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
- assert response_data["owner"] == user.email
+ assert response.data["owner"] == user.email
def test_valid_wiki_page_import_with_extra_data(client):
@@ -902,12 +957,11 @@ def test_valid_wiki_page_import_with_extra_data(client):
"watchers": ["testing@taiga.io"]
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
- assert len(response_data["attachments"]) == 1
- assert response_data["owner"] == user.email
- assert response_data["watchers"] == [user_watching.email]
+ assert len(response.data["attachments"]) == 1
+ assert response.data["owner"] == user.email
+ assert response.data["watchers"] == [user_watching.email]
def test_invalid_wiki_page_import_with_extra_data(client):
@@ -923,13 +977,16 @@ def test_invalid_wiki_page_import_with_extra_data(client):
"attachments": [{}],
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
- response_data = response.data
- assert len(response_data) == 1
+ assert len(response.data) == 1
assert WikiPage.objects.filter(slug="imported-wiki-page").count() == 0
+#######################################################
+## tes api/v1/importer/wiki-link
+#######################################################
+
def test_invalid_wiki_link_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@@ -939,7 +996,7 @@ def test_invalid_wiki_link_import(client):
url = reverse("importer-wiki-link", args=[project.pk])
data = {}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
@@ -955,65 +1012,16 @@ def test_valid_wiki_link_import(client):
"href": "imported-wiki-link",
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
response.data
+##################################################################
+## tes taiga.export_import.services.store_project_from_dict
+##################################################################
-def test_invalid_milestone_import(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- client.login(user)
-
- url = reverse("importer-milestone", args=[project.pk])
- data = {}
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
-
-
-def test_valid_milestone_import(client):
- user = f.UserFactory.create()
- user_watching = f.UserFactory.create(email="testing@taiga.io")
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- client.login(user)
-
- url = reverse("importer-milestone", args=[project.pk])
- data = {
- "name": "Imported milestone",
- "estimated_start": "2014-10-10",
- "estimated_finish": "2014-10-20",
- "watchers": ["testing@taiga.io"]
- }
-
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 201
- assert response.data["watchers"] == [user_watching.email]
-
-def test_milestone_import_duplicated_milestone(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- f.MembershipFactory(project=project, user=user, is_admin=True)
- client.login(user)
-
- url = reverse("importer-milestone", args=[project.pk])
- data = {
- "name": "Imported milestone",
- "estimated_start": "2014-10-10",
- "estimated_finish": "2014-10-20",
- }
- # We create twice the same milestone
- response = client.post(url, json.dumps(data), content_type="application/json")
- response = client.post(url, json.dumps(data), content_type="application/json")
- assert response.status_code == 400
- response_data = response.data
- assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project"
-
-
-def test_dict_to_project_with_no_projects_slots_available(client):
+def test_services_store_project_from_dict_with_no_projects_slots_available(client):
user = f.UserFactory.create(max_private_projects=0)
data = {
@@ -1024,12 +1032,12 @@ def test_dict_to_project_with_no_projects_slots_available(client):
}
with pytest.raises(TaigaImportError) as excinfo:
- project = dict_to_project(data, owner=user)
+ project = services.store_project_from_dict(data, owner=user)
assert "can't have more private projects" in str(excinfo.value)
-def test_dict_to_project_with_no_members_private_project_slots_available(client):
+def test_services_store_project_from_dict_with_no_members_private_project_slots_available(client):
user = f.UserFactory.create(max_memberships_private_projects=2)
data = {
@@ -1059,12 +1067,12 @@ def test_dict_to_project_with_no_members_private_project_slots_available(client)
}
with pytest.raises(TaigaImportError) as excinfo:
- project = dict_to_project(data, owner=user)
+ project = services.store_project_from_dict(data, owner=user)
assert "reaches your current limit of memberships for private" in str(excinfo.value)
-def test_dict_to_project_with_no_members_public_project_slots_available(client):
+def test_services_store_project_from_dict_with_no_members_public_project_slots_available(client):
user = f.UserFactory.create(max_memberships_public_projects=2)
data = {
@@ -1094,11 +1102,15 @@ def test_dict_to_project_with_no_members_public_project_slots_available(client):
}
with pytest.raises(TaigaImportError) as excinfo:
- project = dict_to_project(data, owner=user)
+ project = services.store_project_from_dict(data, owner=user)
assert "reaches your current limit of memberships for public" in str(excinfo.value)
+##################################################################
+## tes api/v1/importer/load-dummp
+##################################################################
+
def test_invalid_dump_import(client):
user = f.UserFactory.create()
client.login(user)
@@ -1110,132 +1122,11 @@ def test_invalid_dump_import(client):
response = client.post(url, {'dump': data})
assert response.status_code == 400
- response_data = response.data
- assert response_data["_error_message"] == "Invalid dump format"
+ assert response.data["_error_message"] == "Invalid dump format"
-def test_valid_dump_import_with_logo(client, settings):
+def test_valid_dump_import_without_enough_public_projects_slots(client, settings):
settings.CELERY_ENABLED = False
-
- user = f.UserFactory.create()
- client.login(user)
-
- url = reverse("importer-load-dump")
-
- data = ContentFile(bytes(json.dumps({
- "slug": "valid-project",
- "name": "Valid project",
- "description": "Valid project desc",
- "is_private": False,
- "logo": {
- "name": "logo.bmp",
- "data": base64.b64encode(DUMMY_BMP_DATA).decode("utf-8")
- }
- }), "utf-8"))
- data.name = "test"
-
- response = client.post(url, {'dump': data})
- assert response.status_code == 201
- response_data = response.data
- assert "id" in response_data
- assert response_data["name"] == "Valid project"
- assert "logo_small_url" in response_data
- assert response_data["logo_small_url"] != None
- assert "logo_big_url" in response_data
- assert response_data["logo_big_url"] != None
-
-
-def test_valid_dump_import_with_celery_disabled(client, settings):
- settings.CELERY_ENABLED = False
-
- user = f.UserFactory.create()
- client.login(user)
-
- url = reverse("importer-load-dump")
-
- data = ContentFile(bytes(json.dumps({
- "slug": "valid-project",
- "name": "Valid project",
- "description": "Valid project desc",
- "is_private": True
- }), "utf-8"))
- data.name = "test"
-
- response = client.post(url, {'dump': data})
- assert response.status_code == 201
- response_data = response.data
- assert "id" in response_data
- assert response_data["name"] == "Valid project"
-
-
-def test_valid_dump_import_with_celery_enabled(client, settings):
- settings.CELERY_ENABLED = True
-
- user = f.UserFactory.create()
- client.login(user)
-
- url = reverse("importer-load-dump")
-
- data = ContentFile(bytes(json.dumps({
- "slug": "valid-project",
- "name": "Valid project",
- "description": "Valid project desc",
- "is_private": True
- }), "utf-8"))
- data.name = "test"
-
- response = client.post(url, {'dump': data})
- assert response.status_code == 202
- response_data = response.data
- assert "import_id" in response_data
-
-
-def test_dump_import_duplicated_project(client):
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- client.login(user)
-
- url = reverse("importer-load-dump")
-
- data = ContentFile(bytes(json.dumps({
- "slug": project.slug,
- "name": "Test import",
- "description": "Valid project desc",
- "is_private": True
- }), "utf-8"))
- data.name = "test"
-
- response = client.post(url, {'dump': data})
- assert response.status_code == 201
- response_data = response.data
- assert response_data["name"] == "Test import"
- assert response_data["slug"] == "{}-test-import".format(user.username)
-
-
-def test_dump_import_throttling(client, settings):
- settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["import-dump-mode"] = "1/minute"
-
- user = f.UserFactory.create()
- project = f.ProjectFactory.create(owner=user)
- client.login(user)
-
- url = reverse("importer-load-dump")
-
- data = ContentFile(bytes(json.dumps({
- "slug": project.slug,
- "name": "Test import",
- "description": "Valid project desc",
- "is_private": True
- }), "utf-8"))
- data.name = "test"
-
- response = client.post(url, {'dump': data})
- assert response.status_code == 201
- response = client.post(url, {'dump': data})
- assert response.status_code == 429
-
-
-def test_valid_dump_import_without_enough_public_projects_slots(client):
user = f.UserFactory.create(max_public_projects=0)
client.login(user)
@@ -1257,7 +1148,8 @@ def test_valid_dump_import_without_enough_public_projects_slots(client):
assert Project.objects.filter(slug="public-project-without-slots").count() == 0
-def test_valid_dump_import_without_enough_private_projects_slots(client):
+def test_valid_dump_import_without_enough_private_projects_slots(client, settings):
+ settings.CELERY_ENABLED = False
user = f.UserFactory.create(max_private_projects=0)
client.login(user)
@@ -1279,7 +1171,8 @@ def test_valid_dump_import_without_enough_private_projects_slots(client):
assert Project.objects.filter(slug="private-project-without-slots").count() == 0
-def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client):
+def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client, settings):
+ settings.CELERY_ENABLED = False
user = f.UserFactory.create(max_memberships_private_projects=5)
client.login(user)
@@ -1326,7 +1219,8 @@ def test_valid_dump_import_without_enough_membership_private_project_slots_one_p
assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0
-def test_valid_dump_import_without_enough_membership_public_project_slots_one_project(client):
+def test_valid_dump_import_without_enough_membership_public_project_slots_one_project(client, settings):
+ settings.CELERY_ENABLED = False
user = f.UserFactory.create(max_memberships_public_projects=5)
client.login(user)
@@ -1424,9 +1318,8 @@ def test_valid_dump_import_with_enough_membership_private_project_slots_multiple
response = client.post(url, {'dump': data})
assert response.status_code == 201
- response_data = response.data
- assert "id" in response_data
- assert response_data["name"] == "Valid project"
+ assert "id" in response.data
+ assert response.data["name"] == "Valid project"
def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_projects(client, settings):
@@ -1480,30 +1373,13 @@ def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_
response = client.post(url, {'dump': data})
assert response.status_code == 201
- response_data = response.data
- assert "id" in response_data
- assert response_data["name"] == "Valid project"
+ assert "id" in response.data
+ assert response.data["name"] == "Valid project"
-def test_valid_dump_import_without_slug(client):
- project = f.ProjectFactory.create(slug="existing-slug")
- user = f.UserFactory.create()
- client.login(user)
- url = reverse("importer-load-dump")
-
- data = ContentFile(bytes(json.dumps({
- "name": "Project name",
- "description": "Valid project desc",
- "is_private": True
- }), "utf-8"))
- data.name = "test"
-
- response = client.post(url, {'dump': data})
- assert response.status_code == 201
-
-
-def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_private_project(client):
+def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_private_project(client, settings):
+ settings.CELERY_ENABLED = False
user = f.UserFactory.create(max_memberships_private_projects=5)
client.login(user)
@@ -1545,7 +1421,8 @@ def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_private_pro
assert Project.objects.filter(slug="private-project-with-memberships-limit-with-you").count() == 1
-def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_public_project(client):
+def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_public_project(client, settings):
+ settings.CELERY_ENABLED = False
user = f.UserFactory.create(max_memberships_public_projects=5)
client.login(user)
@@ -1587,6 +1464,203 @@ def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_public_proj
assert Project.objects.filter(slug="public-project-with-memberships-limit-with-you").count() == 1
+def test_valid_dump_import_with_celery_disabled(client, settings):
+ settings.CELERY_ENABLED = False
+
+ user = f.UserFactory.create()
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "valid-project",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ assert "id" in response.data
+ assert response.data["name"] == "Valid project"
+
+
+def test_invalid_dump_import_with_celery_disabled(client, settings):
+ settings.CELERY_ENABLED = False
+ user = f.UserFactory.create(max_memberships_public_projects=5)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "invalid-project",
+ "name": "Invalid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "memberships": [
+ {
+ "email": user.email,
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ ],
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 400
+
+
+def test_valid_dump_import_with_celery_enabled(client, settings):
+ settings.CELERY_ENABLED = True
+
+ user = f.UserFactory.create()
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "valid-project",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 202
+ assert "import_id" in response.data
+ assert Project.objects.filter(slug="valid-project").count() == 1
+
+
+def test_invalid_dump_import_with_celery_enabled(client, settings):
+ settings.CELERY_ENABLED = True
+ user = f.UserFactory.create(max_memberships_public_projects=5)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "invalid-project",
+ "name": "Invalid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "memberships": [
+ {
+ "email": user.email,
+ "role": "Role",
+ },
+ {
+ "email": "test2@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test3@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test4@test.com",
+ "role": "Role",
+ },
+ {
+ "email": "test5@test.com",
+ "role": "Role",
+ },
+ ],
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 202
+ assert "import_id" in response.data
+ assert Project.objects.filter(slug="invalid-project").count() == 0
+
+
+def test_dump_import_throttling(client, settings):
+ settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["import-dump-mode"] = "1/minute"
+
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": project.slug,
+ "name": "Test import",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 429
+
+
+def test_valid_dump_import_without_slug(client):
+ project = f.ProjectFactory.create(slug="existing-slug")
+ user = f.UserFactory.create()
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "name": "Project name",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+
+
+def test_valid_dump_import_with_logo(client, settings):
+ user = f.UserFactory.create()
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": "valid-project",
+ "name": "Valid project",
+ "description": "Valid project desc",
+ "is_private": False,
+ "logo": {
+ "name": "logo.bmp",
+ "data": base64.b64encode(DUMMY_BMP_DATA).decode("utf-8")
+ }
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ assert "id" in response.data
+ assert response.data["name"] == "Valid project"
+ assert "logo_small_url" in response.data
+ assert response.data["logo_small_url"] != None
+ assert "logo_big_url" in response.data
+ assert response.data["logo_big_url"] != None
+
+
def test_valid_project_import_and_disabled_is_featured(client):
user = f.UserFactory.create()
client.login(user)
@@ -1602,8 +1676,30 @@ def test_valid_project_import_and_disabled_is_featured(client):
"is_featured": True
}
- response = client.post(url, json.dumps(data), content_type="application/json")
+ response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- response_data = response.data
- assert response_data["owner"] == user.email
- assert response_data["is_featured"] == False
+ assert response.data["owner"] == user.email
+ assert response.data["is_featured"] == False
+
+
+def test_dump_import_duplicated_project(client):
+ user = f.UserFactory.create()
+ project = f.ProjectFactory.create(owner=user)
+ client.login(user)
+
+ url = reverse("importer-load-dump")
+
+ data = ContentFile(bytes(json.dumps({
+ "slug": project.slug,
+ "name": "Test import",
+ "description": "Valid project desc",
+ "is_private": True
+ }), "utf-8"))
+ data.name = "test"
+
+ response = client.post(url, {'dump': data})
+ assert response.status_code == 201
+ assert response.data["name"] == "Test import"
+ assert response.data["slug"] == "{}-test-import".format(user.username)
+
+
diff --git a/tests/unit/test_export.py b/tests/unit/test_export.py
index d80103a3..6a4a3ff0 100644
--- a/tests/unit/test_export.py
+++ b/tests/unit/test_export.py
@@ -20,7 +20,7 @@ import io
from .. import factories as f
from taiga.base.utils import json
-from taiga.export_import.service import render_project
+from taiga.export_import.services import render_project
pytestmark = pytest.mark.django_db
From 0db559f9a9c25c52b684a217b40f2fc82e235839 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Mon, 25 Apr 2016 19:46:58 +0200
Subject: [PATCH 18/19] Improve the command 'dump_project'
---
.../management/commands/dump_project.py | 42 ++++++++++++++-----
1 file changed, 31 insertions(+), 11 deletions(-)
diff --git a/taiga/export_import/management/commands/dump_project.py b/taiga/export_import/management/commands/dump_project.py
index dc59a17b..d1248ad4 100644
--- a/taiga/export_import/management/commands/dump_project.py
+++ b/taiga/export_import/management/commands/dump_project.py
@@ -18,25 +18,45 @@
from django.core.management.base import BaseCommand, CommandError
from taiga.projects.models import Project
-from taiga.export_import.renderers import ExportRenderer
from taiga.export_import.services import render_project
-
-import resource
+import os
class Command(BaseCommand):
- args = ''
- help = 'Export a project to json'
- renderer_context = {"indent": 4}
- renderer = ExportRenderer()
+ help = "Export projects to json"
+
+ def add_arguments(self, parser):
+ parser.add_argument("project_slugs",
+ nargs="+",
+ help="")
+
+ parser.add_argument("-d", "--dst_dir",
+ action="store",
+ dest="dst_dir",
+ default="./",
+ metavar="DIR",
+ help="Directory to save the json files. ('./' by default)")
def handle(self, *args, **options):
- for project_slug in args:
+ dst_dir = options["dst_dir"]
+
+ if not os.path.exists(dst_dir):
+ raise CommandError("Directory {} does not exist.".format(dst_dir))
+
+ if not os.path.isdir(dst_dir):
+ raise CommandError("'{}' must be a directory, not a file.".format(dst_dir))
+
+ project_slugs = options["project_slugs"]
+
+ for project_slug in project_slugs:
try:
project = Project.objects.get(slug=project_slug)
except Project.DoesNotExist:
- raise CommandError('Project "%s" does not exist' % project_slug)
+ raise CommandError("Project '{}' does not exist".format(project_slug))
- with open('%s.json'%(project_slug), 'w') as outfile:
- render_project(project, outfile)
+ dst_file = os.path.join(dst_dir, "{}.json".format(project_slug))
+ with open(src_file, "w") as f:
+ render_project(project, f)
+
+ print("-> Generate dump of project '{}' in '{}'".format(project.name, src_file))
From 041e0b6b0b79b64773938403f02d17c03fbcbd48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?=
Date: Thu, 28 Apr 2016 10:35:40 +0200
Subject: [PATCH 19/19] Update CHANGELOG
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7630f9ca..0b5f288a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
# Changelog #
-## 2.1.0 ??? (unreleased)
+## 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)))